Skip to content

Commit

Permalink
Merge branch 'develop' into fix/ws-editor-leave
Browse files Browse the repository at this point in the history
  • Loading branch information
robot9706 authored Sep 19, 2023
2 parents 2df15d0 + 215c3f0 commit 9deb897
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 9 deletions.
2 changes: 1 addition & 1 deletion web/crux-ui/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"/[teamSlug]/projects/[projectId]/versions/[versionId]": ["versions", "images", "deployments"],
"/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId]": ["images", "container"],
"/[teamSlug]/nodes": ["nodes", "tokens"],
"/[teamSlug]/nodes/[nodeId]": ["nodes", "images", "tokens"],
"/[teamSlug]/nodes/[nodeId]": ["nodes", "images", "tokens", "deployments"],
"/[teamSlug]/nodes/[nodeId]/log": [],
"/[teamSlug]/registries": ["registries"],
"/[teamSlug]/registries/[registryId]": ["registries"],
Expand Down
177 changes: 177 additions & 0 deletions web/crux-ui/src/components/nodes/node-deployment-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import DeploymentStatusTag from '@app/components/projects/versions/deployments/deployment-status-tag'
import Filters from '@app/components/shared/filters'
import { DyoCard } from '@app/elements/dyo-card'
import DyoFilterChips from '@app/elements/dyo-filter-chips'
import { DyoHeading } from '@app/elements/dyo-heading'
import DyoIcon from '@app/elements/dyo-icon'
import { DyoList } from '@app/elements/dyo-list'
import DyoModal from '@app/elements/dyo-modal'
import { EnumFilter, enumFilterFor, TextFilter, textFilterFor, useFilters } from '@app/hooks/use-filters'
import { auditFieldGetter, dateSort, enumSort, sortHeaderBuilder, stringSort, useSorting } from '@app/hooks/use-sorting'
import useTeamRoutes from '@app/hooks/use-team-routes'
import { Deployment, DeploymentStatus, DEPLOYMENT_STATUS_VALUES } from '@app/models'
import { auditToLocaleDate } from '@app/utils'
import clsx from 'clsx'
import useTranslation from 'next-translate/useTranslation'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'

interface NodeDeploymentListProps {
deployments: Deployment[]
}

type DeploymentFilter = TextFilter & EnumFilter<DeploymentStatus>
type DeploymentSorting = 'project' | 'version' | 'prefix' | 'updatedAt' | 'status'

const NodeDeploymentList = (props: NodeDeploymentListProps) => {
const { deployments: propsDeployments } = props

const { t } = useTranslation('deployments')
const routes = useTeamRoutes()
const router = useRouter()

const [showInfo, setShowInfo] = useState<Deployment>(null)

const filters = useFilters<Deployment, DeploymentFilter>({
filters: [
textFilterFor<Deployment>(it => [it.project.name, it.version.name, it.prefix]),
enumFilterFor<Deployment, DeploymentStatus>(it => [it.status]),
],
initialData: propsDeployments,
})

const sorting = useSorting<Deployment, DeploymentSorting>(filters.filtered, {
initialField: 'updatedAt',
initialDirection: 'asc',
sortFunctions: {
project: stringSort,
version: stringSort,
prefix: stringSort,
updatedAt: dateSort,
status: enumSort(DEPLOYMENT_STATUS_VALUES),
},
fieldGetters: {
project: it => it.project.name,
version: it => it.version.name,
updatedAt: auditFieldGetter,
},
})

const headers = [
'common:project',
'common:version',
'common:prefix',
'common:updatedAt',
'common:status',
'common:actions',
]
const defaultHeaderClass = 'h-11 uppercase text-bright text-sm bg-medium-eased py-3 px-2 font-semibold'
const headerClasses = [
clsx('rounded-tl-lg pl-6', defaultHeaderClass),
...Array.from({ length: headers.length - 3 }).map(() => defaultHeaderClass),
clsx('text-center', defaultHeaderClass),
clsx('rounded-tr-lg pr-6 text-center', defaultHeaderClass),
]

const defaultItemClass = 'h-11 min-h-min text-light-eased p-2 w-fit'
const itemClasses = [
clsx('pl-6', defaultItemClass),
...Array.from({ length: headerClasses.length - 3 }).map(() => defaultItemClass),
clsx('text-center', defaultItemClass),
clsx('pr-6 text-center', defaultItemClass),
]

const onCellClick = async (data: Deployment, row: number, col: number) => {
if (col >= headers.length - 1) {
return
}

await router.push(routes.deployment.details(data.id))
}

const itemTemplate = (item: Deployment) => /* eslint-disable react/jsx-key */ [
item.project.name,
item.version.name,
<span>{item.prefix}</span>,
<span suppressHydrationWarning>{auditToLocaleDate(item.audit)}</span>,
<DeploymentStatusTag status={item.status} className="w-fit mx-auto" />,
<>
<div className="inline-block mr-2">
<Link href={routes.deployment.details(item.id)} passHref>
<DyoIcon src="/eye.svg" alt={t('common:view')} size="md" />
</Link>
</div>

<DyoIcon
src="/note.svg"
alt={t('common:note')}
size="md"
className={!!item.note && item.note.length > 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30'}
onClick={() => !!item.note && item.note.length > 0 && setShowInfo(item)}
/>
</>,
]
/* eslint-enable react/jsx-key */

return (
<>
{propsDeployments.length ? (
<>
<Filters setTextFilter={it => filters.setFilter({ text: it })}>
<DyoFilterChips
className="pl-6"
choices={DEPLOYMENT_STATUS_VALUES}
converter={it => t(`common:deploymentStatuses.${it}`)}
selection={filters.filter?.enum}
onSelectionChange={type => {
filters.setFilter({
enum: type,
})
}}
/>
</Filters>
<DyoCard className="relative mt-4">
<DyoList
headers={[...headers]}
headerClassName={headerClasses}
itemClassName={itemClasses}
data={sorting.items}
noSeparator
itemBuilder={itemTemplate}
headerBuilder={sortHeaderBuilder<Deployment, DeploymentSorting>(
sorting,
{
'common:project': 'project',
'common:version': 'version',
'common:prefix': 'prefix',
'common:updatedAt': 'updatedAt',
'common:status': 'status',
},
text => t(text),
)}
cellClick={onCellClick}
/>
</DyoCard>
</>
) : (
<DyoHeading element="h3" className="text-md text-center text-light-eased pt-32">
{t('noItems')}
</DyoHeading>
)}
{!showInfo ? null : (
<DyoModal
className="w-1/2 h-1/2"
titleClassName="pl-4 font-medium text-xl text-bright mb-3"
title={t('common:note')}
open={!!showInfo}
onClose={() => setShowInfo(null)}
>
<p className="text-bright mt-8 break-all overflow-y-auto">{showInfo.note}</p>
</DyoModal>
)}
</>
)
}

export default NodeDeploymentList
13 changes: 12 additions & 1 deletion web/crux-ui/src/components/nodes/node-sections-heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,22 @@ const NodeSectionsHeading = (props: NodeSectionsHeadingProps) => {
thin
underlined={section === 'logs'}
textColor="text-bright"
className="ml-6"
className="mx-6"
onClick={() => setSection('logs')}
>
{t('logs')}
</DyoButton>

<DyoButton
text
thin
underlined={section === 'deployments'}
textColor="text-bright"
className="ml-6"
onClick={() => setSection('deployments')}
>
{t('common:deployments')}
</DyoButton>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { useEffect, useState } from 'react'
import { PaginationSettings } from '../shared/paginator'
import useNodeState from './use-node-state'

export type NodeDetailsSection = 'editing' | 'containers' | 'logs'
export type NodeDetailsSection = 'editing' | 'containers' | 'logs' | 'deployments'

export type ContainerTargetStates = { [key: string]: ContainerState } // containerName to targetState

Expand Down
12 changes: 9 additions & 3 deletions web/crux-ui/src/pages/[teamSlug]/nodes/[nodeId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import EditNodeSection from '@app/components/nodes/edit-node-section'
import NodeAuditList from '@app/components/nodes/node-audit-list'
import NodeConnectionCard from '@app/components/nodes/node-connection-card'
import NodeContainersList from '@app/components/nodes/node-containers-list'
import NodeDeploymentList from '@app/components/nodes/node-deployment-list'
import NodeSectionsHeading from '@app/components/nodes/node-sections-heading'
import useNodeDetailsState from '@app/components/nodes/use-node-details-state'
import { BreadcrumbLink } from '@app/components/shared/breadcrumb'
Expand All @@ -13,7 +14,7 @@ import { DetailsPageMenu } from '@app/components/shared/page-menu'
import { DyoConfirmationModal } from '@app/elements/dyo-modal'
import { defaultApiErrorHandler } from '@app/errors'
import useTeamRoutes from '@app/hooks/use-team-routes'
import { NodeDetails } from '@app/models'
import { Deployment, NodeDetails } from '@app/models'
import { TeamRoutes } from '@app/routes'
import { withContextAuthorization } from '@app/utils'
import { getCruxFromContext } from '@server/crux-api'
Expand All @@ -25,10 +26,11 @@ import { useSWRConfig } from 'swr'

interface NodeDetailsPageProps {
node: NodeDetails
deployments: Deployment[]
}

const NodeDetailsPage = (props: NodeDetailsPageProps) => {
const { node: propsNode } = props
const { node: propsNode, deployments } = props

const { t } = useTranslation('nodes')
const routes = useTeamRoutes()
Expand Down Expand Up @@ -113,8 +115,10 @@ const NodeDetailsPage = (props: NodeDetailsPageProps) => {

<NodeContainersList state={state} actions={actions} />
</>
) : (
) : state.section === 'logs' ? (
<NodeAuditList node={node} />
) : (
<NodeDeploymentList deployments={deployments} />
)}
</>
)}
Expand All @@ -132,10 +136,12 @@ const getPageServerSideProps = async (context: NextPageContext) => {
const nodeId = context.query.nodeId as string

const node = await getCruxFromContext<NodeDetails>(context, routes.node.api.details(nodeId))
const deployments = await getCruxFromContext<Deployment[]>(context, routes.node.api.deployments(nodeId))

return {
props: {
node,
deployments,
},
}
}
Expand Down
2 changes: 2 additions & 0 deletions web/crux-ui/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ class NodeApi {

audit = (id: string, query: AuditLogQuery) => urlQuery(`${this.details(id)}/audit`, query)

deployments = (id: string) => `${this.details(id)}/deployments`

// node-global-container
globalContainerList = (id: string) => `${this.details(id)}/containers`

Expand Down
3 changes: 2 additions & 1 deletion web/crux/src/app/deploy/deploy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ export default class DeployService {
return this.deploymentImageEvents.pipe(filter(it => it.deploymentIds.includes(deploymentId)))
}

async getDeployments(teamSlug: string): Promise<DeploymentDto[]> {
async getDeployments(teamSlug: string, nodeId?: string): Promise<DeploymentDto[]> {
const deployments = await this.prisma.deployment.findMany({
where: {
version: {
Expand All @@ -843,6 +843,7 @@ export default class DeployService {
},
},
},
nodeId,
},
include: {
version: {
Expand Down
24 changes: 23 additions & 1 deletion web/crux/src/app/node/node.http.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
import { Identity } from '@ory/kratos-client'
import UuidParams from 'src/decorators/api-params.decorator'
import { CreatedResponse, CreatedWithLocation } from '../../interceptors/created-with-location.decorator'
import { DeploymentDto } from '../deploy/deploy.dto'
import DeployService from '../deploy/deploy.service'
import { DisableAuth, IdentityFromRequest } from '../token/jwt-auth.guard'
import NodeTeamAccessGuard from './guards/node.team-access.http.guard'
import { NodeId, PARAM_NODE_ID, ROUTE_NODES, ROUTE_NODE_ID, ROUTE_TEAM_SLUG, TeamSlug } from './node.const'
Expand All @@ -49,7 +51,10 @@ import NodeGetScriptValidationPipe from './pipes/node.get-script.pipe'
@ApiTags(ROUTE_NODES)
@UseGuards(NodeTeamAccessGuard)
export default class NodeHttpController {
constructor(private service: NodeService) {}
constructor(
private service: NodeService,
private deployService: DeployService,
) {}

@Get()
@HttpCode(HttpStatus.OK)
Expand Down Expand Up @@ -251,4 +256,21 @@ export default class NodeHttpController {
): Promise<NodeAuditLogListDto> {
return await this.service.getAuditLog(nodeId, query)
}

@Get(`${ROUTE_NODE_ID}/deployments`)
@HttpCode(HttpStatus.OK)
@ApiOperation({
description:
'Get the list of deployments. Request needs to include `teamSlug` in URL. A deployment should include `id`, `prefix`, `status`, `note`, `audit` log details, project `name`, `id`, `type`, version `name`, `type`, `id`, and node `name`, `id`, `type`.',
summary: 'Fetch the list of deployments.',
})
@ApiOkResponse({
type: DeploymentDto,
isArray: true,
description: 'List of deployments.',
})
@ApiForbiddenResponse({ description: 'Unauthorized request for deployments.' })
async getDeployments(@TeamSlug() teamSlug: string, @NodeId() nodeId: string): Promise<DeploymentDto[]> {
return await this.deployService.getDeployments(teamSlug, nodeId)
}
}
3 changes: 2 additions & 1 deletion web/crux/src/app/node/node.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import KratosService from 'src/services/kratos.service'
import PrismaService from 'src/services/prisma.service'
import AgentModule from '../agent/agent.module'
import AuditLoggerModule from '../audit.logger/audit.logger.module'
import DeployModule from '../deploy/deploy.module'
import TeamModule from '../team/team.module'
import TeamRepository from '../team/team.repository'
import NodeContainerWebSocketGateway from './node.container.ws.gateway'
Expand All @@ -17,7 +18,7 @@ import NodeService from './node.service'
import NodeWebSocketGateway from './node.ws.gateway'

@Module({
imports: [AgentModule, TeamModule, HttpModule, AuditLoggerModule],
imports: [AgentModule, TeamModule, HttpModule, AuditLoggerModule, DeployModule],
exports: [NodeMapper],
controllers: [NodeHttpController, NodePrefixContainerHttpController, NodeGlobalContainerHttpController],
providers: [
Expand Down

0 comments on commit 9deb897

Please sign in to comment.