Skip to content

Commit

Permalink
feat(web): node paginated deployments list
Browse files Browse the repository at this point in the history
  • Loading branch information
robot9706 committed Dec 3, 2024
1 parent 11bfeb2 commit 3831ba1
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 125 deletions.
263 changes: 155 additions & 108 deletions web/crux-ui/src/components/nodes/node-deployment-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeploymentStatus>
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<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 [filter, setFilter] = useState<FilterState>({
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<PaginatedList<Deployment>> => {
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<Deployment>
},
[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 setTextFilter={it => filters.setFilter({ text: it })}>
<DyoFilterChips
className="pl-6"
name="deploymentStatusFilter"
choices={DEPLOYMENT_STATUS_VALUES}
converter={it => t(deploymentStatusTranslation(it))}
selection={filters.filter?.enum}
onSelectionChange={type => {
filters.setFilter({
enum: type,
})
}}
qaLabel={chipsQALabelFromValue}
/>
</Filters>

<DyoCard className="relative mt-4">
<DyoTable
data={filters.filtered}
dataKey="id"
onRowClick={onRowClick}
initialSortColumn={3}
initialSortDirection="asc"
>
<DyoColumn
header={t('common:project')}
body={(it: Deployment) => it.project.name}
className="w-3/12"
sortable
sortField="project.name"
sort={sortString}
/>
<DyoColumn
header={t('common:version')}
body={(it: Deployment) => it.version.name}
className="w-1/12"
sortable
sortField="version.name"
sort={sortString}
/>
<DyoColumn
header={t('common:prefix')}
field="prefix"
className="w-2/12"
sortable
sortField="prefix"
sort={sortString}
/>
<DyoColumn
header={t('common:updatedAt')}
body={(it: Deployment) => auditToLocaleDate(it.audit)}
className="w-2/12"
suppressHydrationWarning
sortable
sortField={it => it.audit.updatedAt ?? it.audit.createdAt}
sort={sortDate}
/>
<DyoColumn
header={t('common:status')}
body={(it: Deployment) => <DeploymentStatusTag status={it.status} className="w-fit mx-auto" />}
className="text-center"
sortable
sortField="status"
sort={sortEnum(DEPLOYMENT_STATUS_VALUES)}
/>
<DyoColumn
header={t('common:actions')}
className="w-40 text-center"
preventClickThrough
body={(it: Deployment) => (
<>
<div className="inline-block mr-2">
<DyoLink href={routes.deployment.details(it.id)} qaLabel="deployment-list-view-icon">
<DyoIcon src="/eye.svg" alt={t('common:view')} size="md" />
</DyoLink>
</div>

<DyoIcon
src="/note.svg"
alt={t('common:note')}
size="md"
className={!!it.note && it.note.length > 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30'}
onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)}
/>
</>
)}
/>
</DyoTable>
</DyoCard>
</>
) : (
<DyoHeading element="h3" className="text-md text-center text-light-eased pt-32">
{t('noItems')}
</DyoHeading>
)}
<Filters setTextFilter={it => setFilter({ ...filter, filter: it })}>
<DyoFilterChips
className="pl-6"
name="deploymentStatusFilter"
choices={DEPLOYMENT_STATUS_VALUES}
converter={it => t(deploymentStatusTranslation(it))}
selection={filter.status}
onSelectionChange={type => {
setFilter({
...filter,
status: type === 'all' ? null : (type as DeploymentStatus),
})
}}
qaLabel={chipsQALabelFromValue}
/>
</Filters>

<DyoCard className="relative mt-4">
<DyoTable
data={pagination.data ?? []}
dataKey="id"
onRowClick={onRowClick}
paginationTotal={pagination.total}
onServerPagination={setPagination}
initialSortColumn={3}
initialSortDirection="desc"
>
<DyoColumn
header={t('common:project')}
body={(it: Deployment) => it.project.name}
className="w-3/12"
sortable
sortField="project.name"
sort={sortString}
/>
<DyoColumn
header={t('common:version')}
body={(it: Deployment) => it.version.name}
className="w-1/12"
sortable
sortField="version.name"
sort={sortString}
/>
<DyoColumn
header={t('common:prefix')}
field="prefix"
className="w-2/12"
sortable
sortField="prefix"
sort={sortString}
/>
<DyoColumn
header={t('common:updatedAt')}
body={(it: Deployment) => auditToLocaleDate(it.audit)}
className="w-2/12"
suppressHydrationWarning
sortable
sortField={it => it.audit.updatedAt ?? it.audit.createdAt}
sort={sortDate}
/>
<DyoColumn
header={t('common:status')}
body={(it: Deployment) => <DeploymentStatusTag status={it.status} className="w-fit mx-auto" />}
className="text-center"
sortable
sortField="status"
sort={sortEnum(DEPLOYMENT_STATUS_VALUES)}
/>
<DyoColumn
header={t('common:actions')}
className="w-40 text-center"
preventClickThrough
body={(it: Deployment) => (
<>
<div className="inline-block mr-2">
<DyoLink href={routes.deployment.details(it.id)} qaLabel="deployment-list-view-icon">
<DyoIcon src="/eye.svg" alt={t('common:view')} size="md" />
</DyoLink>
</div>

<DyoIcon
src="/note.svg"
alt={t('common:note')}
size="md"
className={!!it.note && it.note.length > 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30'}
onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)}
/>
</>
)}
/>
</DyoTable>
</DyoCard>

{showInfo && (
<DyoModal
Expand Down
7 changes: 6 additions & 1 deletion web/crux-ui/src/models/node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PaginationWithDateQuery } from './common'
import { DeploymentStatus, PaginationQuery, PaginationWithDateQuery } from './common'
import { Container, ContainerCommand, ContainerIdentifier } from './container'

export const NODE_TYPE_VALUES = ['docker', 'k8s'] as const
Expand Down Expand Up @@ -102,6 +102,11 @@ export type NodeContainerInspection = {
inspection: string
}

export type NodeDeploymentQuery = PaginationQuery & {
filter?: string
status?: DeploymentStatus
}

// ws

export const WS_TYPE_NODE_EVENT = 'event'
Expand Down
7 changes: 2 additions & 5 deletions web/crux-ui/src/pages/[teamSlug]/nodes/[nodeId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ import { useSWRConfig } from 'swr'

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

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

const { t } = useTranslation('nodes')
const routes = useTeamRoutes()
Expand Down Expand Up @@ -123,7 +122,7 @@ const NodeDetailsPage = (props: NodeDetailsPageProps) => {
) : state.section === 'logs' ? (
<NodeAuditList node={node} />
) : (
<NodeDeploymentList deployments={deployments} />
<NodeDeploymentList nodeId={propsNode.id} />
)}
</>
)}
Expand All @@ -141,12 +140,10 @@ const getPageServerSideProps = async (context: GetServerSidePropsContext) => {
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
3 changes: 2 additions & 1 deletion web/crux-ui/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ContainerIdentifier,
ContainerOperation,
DeploymentQuery,
NodeDeploymentQuery,
PaginationQuery,
VersionSectionsState,
} from './models'
Expand Down Expand Up @@ -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`

Expand Down
2 changes: 1 addition & 1 deletion web/crux/src/app/deploy/deploy.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
Expand Down
Loading

0 comments on commit 3831ba1

Please sign in to comment.