diff --git a/web/crux-ui/src/components/nodes/node-deployment-list.tsx b/web/crux-ui/src/components/nodes/node-deployment-list.tsx index ef4616541..b871ed2fb 100644 --- a/web/crux-ui/src/components/nodes/node-deployment-list.tsx +++ b/web/crux-ui/src/components/nodes/node-deployment-list.tsx @@ -8,139 +8,186 @@ import DyoIcon from '@app/elements/dyo-icon' import DyoLink from '@app/elements/dyo-link' import DyoModal from '@app/elements/dyo-modal' import DyoTable, { DyoColumn, sortDate, sortEnum, sortString } from '@app/elements/dyo-table' +import { defaultApiErrorHandler } from '@app/errors' import { EnumFilter, TextFilter, enumFilterFor, textFilterFor, useFilters } from '@app/hooks/use-filters' +import usePagination from '@app/hooks/use-pagination' import useTeamRoutes from '@app/hooks/use-team-routes' -import { DEPLOYMENT_STATUS_VALUES, Deployment, DeploymentStatus } from '@app/models' +import { useThrottling } from '@app/hooks/use-throttleing' +import { + DEPLOYMENT_STATUS_VALUES, + Deployment, + DeploymentQuery, + DeploymentStatus, + PaginatedList, + PaginationQuery, +} from '@app/models' import { auditToLocaleDate } from '@app/utils' import useTranslation from 'next-translate/useTranslation' import { useRouter } from 'next/router' import { QA_MODAL_LABEL_DEPLOYMENT_NOTE } from 'quality-assurance' -import { useState } from 'react' +import { useCallback, useEffect, useState } from 'react' +import { PaginationSettings } from '../shared/paginator' + +const defaultPagination: PaginationSettings = { pageNumber: 0, pageSize: 10 } interface NodeDeploymentListProps { - deployments: Deployment[] + nodeId: string } -type DeploymentFilter = TextFilter & EnumFilter +type FilterState = { + filter: string + status: DeploymentStatus | null +} const NodeDeploymentList = (props: NodeDeploymentListProps) => { - const { deployments: propsDeployments } = props + const { nodeId: propsNodeId } = props const { t } = useTranslation('deployments') const routes = useTeamRoutes() const router = useRouter() + const handleApiError = defaultApiErrorHandler(t) + const [showInfo, setShowInfo] = useState(null) - const filters = useFilters({ - filters: [ - textFilterFor(it => [it.project.name, it.version.name, it.prefix]), - enumFilterFor(it => [it.status]), - ], - initialData: propsDeployments, + const [filter, setFilter] = useState({ + filter: '', + status: null, }) + const throttle = useThrottling(1000) + const onRowClick = async (data: Deployment) => await router.push(routes.deployment.details(data.id)) + const fetchData = useCallback( + async (paginationQuery: PaginationQuery): Promise> => { + const { filter: keywordFilter, status } = filter + + const query: DeploymentQuery = { + ...paginationQuery, + filter: !keywordFilter || keywordFilter.trim() === '' ? null : keywordFilter, + status, + } + + const res = await fetch(routes.node.api.deployments(propsNodeId, query)) + + if (!res.ok) { + await handleApiError(res) + return null + } + + return (await res.json()) as PaginatedList + }, + [routes, handleApiError, filter], + ) + + const [pagination, setPagination, refreshPage] = usePagination( + { + defaultSettings: defaultPagination, + fetchData, + }, + [filter], + ) + + useEffect(() => { + throttle(refreshPage) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter]) + return ( <> - {propsDeployments.length ? ( - <> - filters.setFilter({ text: it })}> - t(deploymentStatusTranslation(it))} - selection={filters.filter?.enum} - onSelectionChange={type => { - filters.setFilter({ - enum: type, - }) - }} - qaLabel={chipsQALabelFromValue} - /> - - - - - it.project.name} - className="w-3/12" - sortable - sortField="project.name" - sort={sortString} - /> - it.version.name} - className="w-1/12" - sortable - sortField="version.name" - sort={sortString} - /> - - auditToLocaleDate(it.audit)} - className="w-2/12" - suppressHydrationWarning - sortable - sortField={it => it.audit.updatedAt ?? it.audit.createdAt} - sort={sortDate} - /> - } - className="text-center" - sortable - sortField="status" - sort={sortEnum(DEPLOYMENT_STATUS_VALUES)} - /> - ( - <> -
- - - -
- - 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30'} - onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)} - /> - - )} - /> -
-
- - ) : ( - - {t('noItems')} - - )} + setFilter({ ...filter, filter: it })}> + t(deploymentStatusTranslation(it))} + selection={filter.status} + onSelectionChange={type => { + setFilter({ + ...filter, + status: type === 'all' ? null : (type as DeploymentStatus), + }) + }} + qaLabel={chipsQALabelFromValue} + /> + + + + + it.project.name} + className="w-3/12" + sortable + sortField="project.name" + sort={sortString} + /> + it.version.name} + className="w-1/12" + sortable + sortField="version.name" + sort={sortString} + /> + + auditToLocaleDate(it.audit)} + className="w-2/12" + suppressHydrationWarning + sortable + sortField={it => it.audit.updatedAt ?? it.audit.createdAt} + sort={sortDate} + /> + } + className="text-center" + sortable + sortField="status" + sort={sortEnum(DEPLOYMENT_STATUS_VALUES)} + /> + ( + <> +
+ + + +
+ + 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30'} + onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)} + /> + + )} + /> +
+
{showInfo && ( { - const { node: propsNode, deployments } = props + const { node: propsNode } = props const { t } = useTranslation('nodes') const routes = useTeamRoutes() @@ -123,7 +122,7 @@ const NodeDetailsPage = (props: NodeDetailsPageProps) => { ) : state.section === 'logs' ? ( ) : ( - + )} )} @@ -141,12 +140,10 @@ const getPageServerSideProps = async (context: GetServerSidePropsContext) => { const nodeId = context.query.nodeId as string const node = await getCruxFromContext(context, routes.node.api.details(nodeId)) - const deployments = await getCruxFromContext(context, routes.node.api.deployments(nodeId)) return { props: { node, - deployments, }, } } diff --git a/web/crux-ui/src/routes.ts b/web/crux-ui/src/routes.ts index 99b505fc3..7521c9e04 100644 --- a/web/crux-ui/src/routes.ts +++ b/web/crux-ui/src/routes.ts @@ -5,6 +5,7 @@ import { ContainerIdentifier, ContainerOperation, DeploymentQuery, + NodeDeploymentQuery, PaginationQuery, VersionSectionsState, } from './models' @@ -237,7 +238,7 @@ class NodeApi { audit = (id: string, query: AuditLogQuery) => urlQuery(`${this.details(id)}/audit`, query) - deployments = (id: string) => `${this.details(id)}/deployments` + deployments = (id: string, query?: NodeDeploymentQuery) => urlQuery(`${this.details(id)}/deployments`, query) kick = (id: string) => `${this.details(id)}/kick` diff --git a/web/crux/src/app/deploy/deploy.dto.ts b/web/crux/src/app/deploy/deploy.dto.ts index 1e2478b3c..d714ce0d5 100644 --- a/web/crux/src/app/deploy/deploy.dto.ts +++ b/web/crux/src/app/deploy/deploy.dto.ts @@ -28,7 +28,7 @@ import { BasicNodeDto, BasicNodeWithStatus } from '../node/node.dto' import { BasicProjectDto } from '../project/project.dto' import { BasicVersionDto } from '../version/version.dto' -const DEPLOYMENT_STATUS_VALUES = ['preparing', 'in-progress', 'successful', 'failed', 'obsolete'] as const +export const DEPLOYMENT_STATUS_VALUES = ['preparing', 'in-progress', 'successful', 'failed', 'obsolete'] as const export type DeploymentStatusDto = (typeof DEPLOYMENT_STATUS_VALUES)[number] export type EnvironmentToConfigBundleNameMap = Record diff --git a/web/crux/src/app/deploy/deploy.http.controller.ts b/web/crux/src/app/deploy/deploy.http.controller.ts index 67cc096ec..0662a4599 100644 --- a/web/crux/src/app/deploy/deploy.http.controller.ts +++ b/web/crux/src/app/deploy/deploy.http.controller.ts @@ -18,14 +18,12 @@ import { ApiBody, ApiConflictResponse, ApiCreatedResponse, - ApiExtraModels, ApiForbiddenResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, - refs, } from '@nestjs/swagger' import { Identity } from '@ory/kratos-client' import UuidParams from 'src/decorators/api-params.decorator' @@ -90,10 +88,7 @@ export default class DeployHttpController { }) @ApiOkResponse({ type: DeploymentQueryDto, description: 'Paginated list of deployments.' }) @ApiForbiddenResponse({ description: 'Unauthorized request for deployments.' }) - async getDeployments( - @TeamSlug() teamSlug: string, - @Query() query: DeploymentQueryDto, - ): Promise { + async getDeployments(@TeamSlug() teamSlug: string, @Query() query: DeploymentQueryDto): Promise { return await this.service.getDeployments(teamSlug, query) } diff --git a/web/crux/src/app/node/node.dto.ts b/web/crux/src/app/node/node.dto.ts index 8dc8635b6..9a395fe8d 100755 --- a/web/crux/src/app/node/node.dto.ts +++ b/web/crux/src/app/node/node.dto.ts @@ -15,6 +15,7 @@ import { import { CONTAINER_STATE_VALUES, ContainerState } from 'src/domain/container' import { PaginatedList, PaginationQuery } from 'src/shared/dtos/paginating' import { ContainerIdentifierDto } from '../container/container.dto' +import { DEPLOYMENT_STATUS_VALUES, DeploymentStatusDto } from '../deploy/deploy.dto' export const NODE_SCRIPT_TYPE_VALUES = ['shell', 'powershell'] as const export type NodeScriptTypeDto = (typeof NODE_SCRIPT_TYPE_VALUES)[number] @@ -264,3 +265,16 @@ export class NodeContainerLogQuery { @Type(() => Number) take?: number } + +export class NodeDeploymentQueryDto extends PaginationQuery { + @IsOptional() + @IsString() + @Type(() => String) + @ApiProperty() + readonly filter?: string + + @IsOptional() + @ApiProperty({ enum: DEPLOYMENT_STATUS_VALUES }) + @IsIn(DEPLOYMENT_STATUS_VALUES) + readonly status?: DeploymentStatusDto +} diff --git a/web/crux/src/app/node/node.http.controller.ts b/web/crux/src/app/node/node.http.controller.ts index 8847b80d9..d8aabcfe0 100644 --- a/web/crux/src/app/node/node.http.controller.ts +++ b/web/crux/src/app/node/node.http.controller.ts @@ -27,7 +27,7 @@ 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, DeploymentListDto } from '../deploy/deploy.dto' +import { DeploymentListDto } 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' @@ -36,6 +36,7 @@ import { CreateNodeDto, NodeAuditLogListDto, NodeAuditLogQueryDto, + NodeDeploymentQueryDto, NodeDetailsDto, NodeDto, NodeGenerateScriptDto, @@ -46,7 +47,6 @@ import NodeService from './node.service' import DeleteNodeValidationPipe from './pipes/node.delete.pipe' import NodeGenerateScriptValidationPipe from './pipes/node.generate-script.pipe' import NodeGetScriptValidationPipe from './pipes/node.get-script.pipe' -import { PaginationQuery } from 'src/shared/dtos/paginating' @Controller(`${ROUTE_TEAM_SLUG}/${ROUTE_NODES}`) @ApiTags(ROUTE_NODES) @@ -270,7 +270,11 @@ export default class NodeHttpController { description: 'Paginated list of deployments.', }) @ApiForbiddenResponse({ description: 'Unauthorized request for deployments.' }) - async getDeployments(@TeamSlug() teamSlug: string, @NodeId() nodeId: string, @Query() query: PaginationQuery): Promise { + async getDeployments( + @TeamSlug() teamSlug: string, + @NodeId() nodeId: string, + @Query() query: NodeDeploymentQueryDto, + ): Promise { return await this.deployService.getDeployments(teamSlug, { ...query, nodeId,