diff --git a/web/crux-ui/src/components/projects/versions/images/edit-image-tags.tsx b/web/crux-ui/src/components/projects/versions/images/edit-image-tags.tsx index 423aa3156..f14c9a2f3 100644 --- a/web/crux-ui/src/components/projects/versions/images/edit-image-tags.tsx +++ b/web/crux-ui/src/components/projects/versions/images/edit-image-tags.tsx @@ -1,72 +1,78 @@ import { DyoHeading } from '@app/elements/dyo-heading' import { DyoInput } from '@app/elements/dyo-input' +import { DyoLabel } from '@app/elements/dyo-label' import DyoMessage from '@app/elements/dyo-message' import DyoRadioButton from '@app/elements/dyo-radio-button' +import LoadingIndicator from '@app/elements/loading-indicator' import { TextFilter, textFilterFor, useFilters } from '@app/hooks/use-filters' import { RegistryImageTag } from '@app/models' +import { utcDateToLocale } from '@app/utils' import useTranslation from 'next-translate/useTranslation' import { useEffect, useMemo, useState } from 'react' -import TagSortToggle, { SortState } from './tag-sort-toggle' -import LoadingIndicator from '@app/elements/loading-indicator' -import { DyoLabel } from '@app/elements/dyo-label' -import DyoIndicator from '@app/elements/dyo-indicator' +import TagSortToggle, { TagSortState } from './tag-sort-toggle' -interface ImageTagInputProps { +type ImageTagInputProps = { disabled?: boolean selected: string onTagSelected: (tag: string) => void } -type ImageTagSelectListProps = ImageTagInputProps & { - tags: Record +type SelectImageTagListProps = ImageTagInputProps & { + tags: RegistryImageTag[] loadingTags: boolean } -type Entry = [string, RegistryImageTag] - -const ImageTagSelectList = (props: ImageTagSelectListProps) => { +const SelectImageTagList = (props: SelectImageTagListProps) => { const { disabled, tags, selected: propsSelected, onTagSelected, loadingTags } = props const { t } = useTranslation('images') const [selected, setSelected] = useState(propsSelected) - const [sortState, setSortState] = useState({ + const [sortState, setSortState] = useState({ mode: 'date', - dir: 1, + direction: 'desc', }) - const filters = useFilters({ - filters: [textFilterFor(it => [it[0]])], - initialData: Object.entries(tags), + const filters = useFilters({ + filters: [textFilterFor(it => [it.name, it.created])], + initialData: tags, initialFilter: { text: '', }, }) - useEffect(() => filters.setItems(Object.entries(tags)), [tags]) + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => filters.setItems(tags), [tags]) const sortedItems = useMemo(() => { + const dir = sortState.direction === 'asc' ? -1 : 1 + const items = filters.filtered + switch (sortState.mode) { - case 'alphabetic': - return items.sort((a, b) => b[0].localeCompare(a[0]) * sortState.dir) + case 'alphabetical': + return items.sort((one, other) => other.name.localeCompare(one.name) * dir) case 'date': - return items.sort((a, b) => { - const aDate = Date.parse(a[1].created) - const bDate = Date.parse(b[1].created) + return items.sort((one, other) => { + const oneDate = Date.parse(one.created) + const otherDate = Date.parse(other.created) - return Math.sign(bDate - aDate) * sortState.dir + return Math.sign(otherDate - oneDate) * dir }) default: return items } }, [sortState, filters.filtered]) - const isTagNewer = (tagIndex: number, currentTagIndex: number) => - currentTagIndex >= 0 && - ((sortState.dir === -1 && currentTagIndex < tagIndex) || (sortState.dir === 1 && currentTagIndex > tagIndex)) + const selectedTag = tags.find(it => it.name === selected) ?? null + + const newerThanSelected = (tag: RegistryImageTag): boolean => { + if (!selectedTag?.created) { + return false + } - const selectedTagIndex = selected ? sortedItems.findIndex(it => it[0] === selected) : -1 + return Date.parse(tag.created) > Date.parse(selectedTag.created) + } return (
@@ -92,27 +98,34 @@ const ImageTagSelectList = (props: ImageTagSelectListProps) => { ) : ( <> {selected ? null : } -
+
{sortedItems.map((it, index) => ( - { - setSelected(it[0]) - onTagSelected(it[0]) - }} - qaLabel={`imageTag-${index}`} - labelTemplate={label => ( - <> - {isTagNewer(index, selectedTagIndex) && ( - - )} - {label} - - )} - /> +
+ { + setSelected(it.name) + onTagSelected(it.name) + }} + qaLabel={`image-tag-${index}`} + labelTemplate={label => ( + <> + {label} + + {newerThanSelected(it) && ( + + {t('common:new')} + + )} + + )} + /> + + {it.created && {utcDateToLocale(it.created)}} +
))}
@@ -126,7 +139,7 @@ const ImageTagInput = (props: ImageTagInputProps) => { const { t } = useTranslation('images') - const [selected, setSelected] = useState(propsSelected) + const [selected, setSelected] = useState(propsSelected ?? '') return (
@@ -146,14 +159,15 @@ const ImageTagInput = (props: ImageTagInputProps) => { messageType="info" message={!selected.length && t('tagRequired')} /> +

{t('uncheckedRegistryExplanation')}

) } -const EditImageTags = (props: ImageTagSelectListProps) => { +const EditImageTags = (props: SelectImageTagListProps) => { const { tags } = props - return tags ? : + return tags ? : } export default EditImageTags diff --git a/web/crux-ui/src/components/projects/versions/images/tag-sort-toggle.tsx b/web/crux-ui/src/components/projects/versions/images/tag-sort-toggle.tsx index 1b4ec7ba2..1f27ddf8e 100644 --- a/web/crux-ui/src/components/projects/versions/images/tag-sort-toggle.tsx +++ b/web/crux-ui/src/components/projects/versions/images/tag-sort-toggle.tsx @@ -1,50 +1,41 @@ import clsx from 'clsx' -import useTranslation from 'next-translate/useTranslation' import Image from 'next/image' -export const SORT_MODES = ['alphabetic', 'date'] as const -export type SortModesEnum = (typeof SORT_MODES)[number] +export const SORT_MODES = ['alphabetical', 'date'] as const +export type TagSortMode = (typeof SORT_MODES)[number] -export type SortState = { - mode: SortModesEnum - dir: -1 | 1 +export type TagSortDirection = 'asc' | 'desc' + +export type TagSortState = { + mode: TagSortMode + direction: TagSortDirection } type TagSortToggleProps = { className?: string - state: SortState - onStateChange: (state: SortState) => void + state: TagSortState + onStateChange: (state: TagSortState) => void } -const SORT_ICONS: Record = { - alphabetic: { - '1': '/sort-alphabetical-asc.svg', - '-1': '/sort-alphabetical-desc.svg', - }, - date: { - '1': '/sort-date-asc.svg', - '-1': '/sort-date-desc.svg', - }, -} +const iconOf = (mode: TagSortMode, direction: TagSortDirection) => `/sort-${mode}-${direction}.svg` const TagSortToggle = (props: TagSortToggleProps) => { const { className, state, onStateChange } = props - const { mode, dir } = state - - const { t } = useTranslation('common') + const { mode, direction } = state - const onToggleMode = (newMode: SortModesEnum) => { - if (mode === newMode) { - onStateChange({ - mode, - dir: dir == 1 ? -1 : 1, - }) - } else { + const onSortIconClick = (newMode: TagSortMode) => { + if (mode !== newMode) { onStateChange({ mode: newMode, - dir, + direction, }) + return } + + onStateChange({ + mode, + direction: direction === 'asc' ? 'desc' : 'asc', + }) } return ( @@ -58,9 +49,9 @@ const TagSortToggle = (props: TagSortToggleProps) => {
onToggleMode(it)} + onClick={() => onSortIconClick(it)} > - {mode} + {mode}
))}
diff --git a/web/crux-ui/src/components/projects/versions/use-version-state.ts b/web/crux-ui/src/components/projects/versions/use-version-state.ts index 679f01c80..a8f052b31 100644 --- a/web/crux-ui/src/components/projects/versions/use-version-state.ts +++ b/web/crux-ui/src/components/projects/versions/use-version-state.ts @@ -18,7 +18,7 @@ import { OrderImagesMessage, PatchVersionImage, RegistryImages, - RegistryImageTags, + RegistryImageTag, RegistryImageTagsMessage, VersionDetails, VersionImage, @@ -28,7 +28,6 @@ import { WS_TYPE_GET_IMAGE, WS_TYPE_IMAGE, WS_TYPE_IMAGE_DELETED, - WS_TYPE_SET_IMAGE_TAG, WS_TYPE_IMAGE_TAG_UPDATED, WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGES_WERE_REORDERED, @@ -37,7 +36,7 @@ import { WS_TYPE_PATCH_RECEIVED, WS_TYPE_REGISTRY_FETCH_IMAGE_TAGS, WS_TYPE_REGISTRY_IMAGE_TAGS, - RegistryImageTag, + WS_TYPE_SET_IMAGE_TAG, } from '@app/models' import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' import useTranslation from 'next-translate/useTranslation' @@ -45,7 +44,7 @@ import { useEffect, useState } from 'react' import useCopyDeploymentState from '../../deployments/use-copy-deployment-state' // state -export type ImageTagsMap = { [key: string]: RegistryImageTags } // image key to RegistryImageTags +export type ImageTagsMap = Record> // registryId to imageName to RegistryImageTag list export type VersionAddSection = 'image' | 'deployment' | 'copy-deployment' | 'none' @@ -87,8 +86,6 @@ export type VersionActions = { onDeploymentDeleted: (deploymentId: string) => void } -export const imageTagKey = (registryId: string, imageName: string) => `${registryId}/${imageName}` - const mergeImagePatch = (oldImage: VersionImage, newImage: PatchVersionImage): VersionImage => ({ ...oldImage, ...newImage, @@ -127,15 +124,14 @@ const refreshImageTags = (registriesSock: WebSocketClientEndpoint, images: Versi }) } -export const selectTagsOfImage = (state: VerionState, image: VersionImage): Record => { - const regImgTags = state.tags[imageTagKey(image.registry.id, image.name)] - return regImgTags - ? regImgTags.tags - : image.tag - ? { - [image.tag]: null, - } - : {} +export const selectTagsOfImage = (state: VerionState, image: VersionImage): RegistryImageTag[] | null => { + const images = state.tags[image.registry.id] + if (!images) { + return null + } + + const tags = images[image.name] + return tags ?? null } export const useVersionState = (options: VersionStateOptions): [VerionState, VersionActions] => { @@ -192,12 +188,18 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver return } - const newTags = { ...tags } - message.images.forEach(it => { - const key = imageTagKey(message.registryId, it.name) - newTags[key] = it + const newTagMap = { ...tags } + let images = newTagMap[message.registryId] + if (!images) { + images = {} + newTagMap[message.registryId] = images + } + + message.images.forEach(img => { + images[img.name] = img.tags }) - setTags(newTags) + + setTags(newTagMap) }) versionSock.on(WS_TYPE_IMAGES_WERE_REORDERED, (message: ImagesWereReorderedMessage) => { @@ -283,15 +285,13 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver setSection('images') } - const fetchImageTags = (image: VersionImage): RegistryImageTags => { + const fetchImageTags = (image: VersionImage) => { if (image.registry.type === 'unchecked') { return } - const key = imageTagKey(image.registry.id, image.name) - const imgTags = tags[key] - - if (imgTags) { + const images = tags[image.registry.id] + if (images && images[image.name]) { return } diff --git a/web/crux-ui/src/components/projects/versions/version-view-list.tsx b/web/crux-ui/src/components/projects/versions/version-view-list.tsx index 446240bb8..73d477227 100644 --- a/web/crux-ui/src/components/projects/versions/version-view-list.tsx +++ b/web/crux-ui/src/components/projects/versions/version-view-list.tsx @@ -146,9 +146,9 @@ const VersionViewList = (props: VersionViewListProps) => { qaLabel={QA_MODAL_LABEL_IMAGE_TAGS} > actions.selectTagForImage(tagsModalTarget, it)} /> diff --git a/web/crux-ui/src/models/registry.ts b/web/crux-ui/src/models/registry.ts index 39212549e..15d6646b2 100644 --- a/web/crux-ui/src/models/registry.ts +++ b/web/crux-ui/src/models/registry.ts @@ -195,18 +195,19 @@ export const WS_TYPE_REGISTRY_FETCH_IMAGE_TAGS = 'fetch-image-tags' export type FetchImageTagsMessage = RegistryImages export type RegistryImageTag = { + name: string created: string } -export type RegistryImageTags = { +export type RegistryImageWithTags = { name: string - tags: Record + tags: RegistryImageTag[] } export const WS_TYPE_REGISTRY_IMAGE_TAGS = 'registry-image-tags' export type RegistryImageTagsMessage = { registryId: string - images: RegistryImageTags[] + images: RegistryImageWithTags[] } // mappers diff --git a/web/crux-ui/src/routes.ts b/web/crux-ui/src/routes.ts index 6acee58da..e35d9d292 100644 --- a/web/crux-ui/src/routes.ts +++ b/web/crux-ui/src/routes.ts @@ -237,7 +237,7 @@ class NodeApi { audit = (id: string, query: AuditLogQuery) => urlQuery(`${this.details(id)}/audit`, query) - deployments = (id: string, query?: NodeDeploymentQuery) => urlQuery(`${this.details(id)}/deployments`, query) + deployments = (id: string) => `${this.details(id)}/deployments` kick = (id: string) => `${this.details(id)}/kick` diff --git a/web/crux-ui/src/validations/container.ts b/web/crux-ui/src/validations/container.ts index 5e9f7ebd7..0ea06309f 100644 --- a/web/crux-ui/src/validations/container.ts +++ b/web/crux-ui/src/validations/container.ts @@ -470,7 +470,7 @@ const testRules = ( fieldName: string, ) => { if (rules.length < 1) { - return null + return true } const requiredKeys = rules.map(([key]) => key) @@ -504,13 +504,13 @@ const testRules = ( return err } - return null + return true } const testEnvironmentRules = (imageLabels: Record) => (envs: UniqueKeyValue[]) => { const rules = parseDyrectorioEnvRules(imageLabels) if (!rules) { - return null + return true } const requiredRules = Object.entries(rules).filter(([, rule]) => rule.required) @@ -522,7 +522,7 @@ const testEnvironmentRules = (imageLabels: Record) => (envs: Uni const testSecretRules = (imageLabels: Record) => (secrets: UniqueSecretKeyValue[]) => { const rules = parseDyrectorioEnvRules(imageLabels) if (!rules) { - return null + return true } const requiredRules = Object.entries(rules).filter(([, rule]) => rule.required) diff --git a/web/crux/src/app/node/node.dto.ts b/web/crux/src/app/node/node.dto.ts index 6c4588768..6da84e834 100755 --- a/web/crux/src/app/node/node.dto.ts +++ b/web/crux/src/app/node/node.dto.ts @@ -15,7 +15,6 @@ 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] @@ -272,16 +271,3 @@ 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 850eaf2dc..aaca859ed 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 { 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' @@ -265,11 +265,12 @@ export default class NodeHttpController { summary: 'Fetch the list of deployments.', }) @ApiOkResponse({ - type: DeploymentListDto, - description: 'Paginated list of deployments.', + type: DeploymentDto, + isArray: true, + description: 'List of deployments.', }) @ApiForbiddenResponse({ description: 'Unauthorized request for deployments.' }) - async getDeployments(@TeamSlug() teamSlug: string, @NodeId() nodeId: string): Promise { + async getDeployments(@TeamSlug() _: string, @NodeId() nodeId: string): Promise { const deployments = await this.deployService.getDeploymentsByNodeId(nodeId) return deployments diff --git a/web/crux/src/app/registry/registry-clients/cached-hub-api-client.ts b/web/crux/src/app/registry/registry-clients/cached-hub-api-client.ts index 5f93fa73b..6acd6aa3a 100644 --- a/web/crux/src/app/registry/registry-clients/cached-hub-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/cached-hub-api-client.ts @@ -1,5 +1,5 @@ import { Cache } from 'cache-manager' -import { RegistryImageTag, RegistryImageTags } from '../registry.message' +import { RegistryImageTag, RegistryImageWithTags } from '../registry.message' import HubApiCache from './caches/hub-api-cache' import HubApiClient from './hub-api-client' import { RegistryApiClient } from './registry-api-client' @@ -29,7 +29,7 @@ export default class CachedPublicHubApiClient extends HubApiClient implements Re return repositories.filter(it => it.includes(text)) } - async tags(image: string): Promise { + async tags(image: string): Promise { let tags: string[] = this.hubCache.get(image) if (!tags) { tags = await this.fetchTags(image) @@ -38,15 +38,9 @@ export default class CachedPublicHubApiClient extends HubApiClient implements Re } // NOTE(@robot9706): Docker ratelimits us so skip tag info for now - const tagsWithInfo = tags.reduce( - (map, it) => { - map[it] = { - created: null, - } - return map - }, - {} as Record, - ) + const tagsWithInfo: RegistryImageTag[] = tags.map(it => ({ + name: it, + })) return { name: image, diff --git a/web/crux/src/app/registry/registry-clients/github-api-client.ts b/web/crux/src/app/registry/registry-clients/github-api-client.ts index 3a6e8ff29..2d585620e 100644 --- a/web/crux/src/app/registry/registry-clients/github-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/github-api-client.ts @@ -2,8 +2,8 @@ import { Cache } from 'cache-manager' import { getRegistryApiException } from 'src/exception/registry-exception' import { REGISTRY_GITHUB_URL } from 'src/shared/const' import { GithubNamespace } from '../registry.dto' -import { RegistryImageTag, RegistryImageTags } from '../registry.message' -import { RegistryApiClient, fetchInfoForTags } from './registry-api-client' +import { RegistryImageWithTags } from '../registry.message' +import { RegistryApiClient, RegistryImageTagInfo, fetchInfoForTags } from './registry-api-client' import V2HttpApiClient from './v2-http-api-client' import RegistryV2ApiClient, { RegistryV2ApiClientOptions, @@ -50,7 +50,7 @@ class GithubRegistryClient implements RegistryApiClient { return repositories.filter(it => it.includes(text)) } - async tags(image: string): Promise { + async tags(image: string): Promise { const tokenRes = await fetch( `https://${REGISTRY_GITHUB_URL}/token?service=${REGISTRY_GITHUB_URL}&scope=repository:${this.imageNamePrefix}/${image}:pull`, { @@ -104,7 +104,7 @@ class GithubRegistryClient implements RegistryApiClient { return this.createApiClient().fetchLabels(image, tag) } - async tagInfo(image: string, tag: string): Promise { + async tagInfo(image: string, tag: string): Promise { return this.createApiClient().fetchTagInfo(image, tag) } } diff --git a/web/crux/src/app/registry/registry-clients/gitlab-api-client.ts b/web/crux/src/app/registry/registry-clients/gitlab-api-client.ts index e78142ef6..f306590ca 100644 --- a/web/crux/src/app/registry/registry-clients/gitlab-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/gitlab-api-client.ts @@ -1,8 +1,8 @@ import { Cache } from 'cache-manager' import { getRegistryApiException } from 'src/exception/registry-exception' import { GitlabNamespace } from '../registry.dto' -import { RegistryImageTag, RegistryImageTags } from '../registry.message' -import { RegistryApiClient, fetchInfoForTags } from './registry-api-client' +import { RegistryImageWithTags } from '../registry.message' +import { RegistryApiClient, RegistryImageTagInfo, fetchInfoForTags } from './registry-api-client' import V2HttpApiClient from './v2-http-api-client' import RegistryV2ApiClient, { RegistryV2ApiClientOptions } from './v2-registry-api-client' @@ -58,7 +58,7 @@ export class GitlabRegistryClient implements RegistryApiClient { return repositories.filter(it => it.includes(text)) } - async tags(image: string): Promise { + async tags(image: string): Promise { const tokenRes = await fetch( `https://${this.urls.apiUrl}/jwt/auth?service=container_registry&scope=repository:${image}:pull`, { @@ -109,7 +109,7 @@ export class GitlabRegistryClient implements RegistryApiClient { return this.createApiClient().fetchLabels(image, tag) } - async tagInfo(image: string, tag: string): Promise { + async tagInfo(image: string, tag: string): Promise { return this.createApiClient().fetchTagInfo(image, tag) } } diff --git a/web/crux/src/app/registry/registry-clients/google-api-client.ts b/web/crux/src/app/registry/registry-clients/google-api-client.ts index db0430d79..9b42101bf 100644 --- a/web/crux/src/app/registry/registry-clients/google-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/google-api-client.ts @@ -3,8 +3,8 @@ import { JWT } from 'google-auth-library' import { GetAccessTokenResponse } from 'google-auth-library/build/src/auth/oauth2client' import { CruxUnauthorizedException } from 'src/exception/crux-exception' import { getRegistryApiException } from 'src/exception/registry-exception' -import { RegistryImageTag, RegistryImageTags } from '../registry.message' -import { RegistryApiClient, fetchInfoForTags } from './registry-api-client' +import { RegistryImageTag, RegistryImageWithTags } from '../registry.message' +import { RegistryApiClient, RegistryImageTagInfo, fetchInfoForTags } from './registry-api-client' import V2HttpApiClient from './v2-http-api-client' export type GoogleClientOptions = { @@ -76,7 +76,7 @@ export class GoogleRegistryClient implements RegistryApiClient { return json.child.filter(it => it.includes(text)) } - async tags(image: string): Promise { + async tags(image: string): Promise { if (this.client) { await this.registryCredentialsToBearerAuth() } @@ -92,11 +92,11 @@ export class GoogleRegistryClient implements RegistryApiClient { } const json = (await tagRes.json()) as { tags: string[] } - const tagInfo = await fetchInfoForTags(image, json.tags, this) + const tags = await fetchInfoForTags(image, json.tags, this) return { name: image, - tags: tagInfo, + tags, } } @@ -124,7 +124,7 @@ export class GoogleRegistryClient implements RegistryApiClient { return client.fetchLabels(image, tag) } - async tagInfo(image: string, tag: string): Promise { + async tagInfo(image: string, tag: string): Promise { const client = await this.createApiClient() return client.fetchTagInfo(image, tag) diff --git a/web/crux/src/app/registry/registry-clients/hub-api-client.ts b/web/crux/src/app/registry/registry-clients/hub-api-client.ts index 361021b6e..7284719d7 100644 --- a/web/crux/src/app/registry/registry-clients/hub-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/hub-api-client.ts @@ -1,6 +1,6 @@ import { Cache } from 'cache-manager' import { getRegistryApiException } from 'src/exception/registry-exception' -import { RegistryImageTag } from '../registry.message' +import { RegistryImageTagInfo } from './registry-api-client' import V2HttpApiClient from './v2-http-api-client' type HubApiPaginatedResponse = { @@ -92,13 +92,15 @@ export default abstract class HubApiClient { ) } + // eslint-disable-next-line @typescript-eslint/no-unused-vars async labels(image: string, tag: string): Promise> { // NOTE(@robot9706): Docker ratelimits us so skip this for now // return this.createApiClient().fetchLabels(image, tag) return {} } - async tagInfo(image: string, tag: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async tagInfo(image: string, tag: string): Promise { // NOTE(@robot9706): Docker ratelimits us so skip this for now // return this.createApiClient().fetchTagInfo(image, tag) return { diff --git a/web/crux/src/app/registry/registry-clients/private-hub-api-client.ts b/web/crux/src/app/registry/registry-clients/private-hub-api-client.ts index 0e27be647..674220618 100644 --- a/web/crux/src/app/registry/registry-clients/private-hub-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/private-hub-api-client.ts @@ -1,6 +1,6 @@ import { Cache } from 'cache-manager' import { CruxUnauthorizedException } from 'src/exception/crux-exception' -import { RegistryImageTag, RegistryImageTags } from '../registry.message' +import { RegistryImageTag, RegistryImageWithTags } from '../registry.message' import HubApiClient, { DOCKER_HUB_REGISTRY_URL } from './hub-api-client' import { RegistryApiClient } from './registry-api-client' import V2HttpApiClient from './v2-http-api-client' @@ -57,19 +57,13 @@ export default class PrivateHubApiClient extends HubApiClient implements Registr return repositories.filter(it => it.includes(text)) } - async tags(image: string): Promise { + async tags(image: string): Promise { const tags = await super.fetchTags(image) // NOTE(@robot9706): Docker ratelimits us so skip tag info for now - const tagsWithInfo = tags.reduce( - (map, it) => { - map[it] = { - created: null, - } - return map - }, - {} as Record, - ) + const tagsWithInfo: RegistryImageTag[] = tags.map(it => ({ + name: it, + })) return { name: image, diff --git a/web/crux/src/app/registry/registry-clients/registry-api-client.ts b/web/crux/src/app/registry/registry-clients/registry-api-client.ts index aebea6655..66a61602f 100644 --- a/web/crux/src/app/registry/registry-clients/registry-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/registry-api-client.ts @@ -1,31 +1,29 @@ -import { RegistryImageTag, RegistryImageTags } from '../registry.message' +import { RegistryImageTag, RegistryImageWithTags } from '../registry.message' + +export type RegistryImageTagInfo = { + created: string +} export interface RegistryApiClient { catalog(text: string): Promise - tags(image: string): Promise + tags(image: string): Promise labels(image: string, tag: string): Promise> - tagInfo(image: string, tag: string): Promise + tagInfo(image: string, tag: string): Promise } export const fetchInfoForTags = async ( image: string, tags: string[], client: RegistryApiClient, -): Promise> => { - const tagsWithInfoPromise = tags.map(async it => { - const info = await client.tagInfo(image, it) +): Promise => { + const tagsWithInfo = tags.map(async tag => { + const info = await client.tagInfo(image, tag) return { - tag: it, - info, + ...info, + name: tag, } }) - return (await Promise.all(tagsWithInfoPromise)).reduce( - (map, it) => { - map[it.tag] = it.info - return map - }, - {} as Record, - ) + return await Promise.all(tagsWithInfo) } diff --git a/web/crux/src/app/registry/registry-clients/unchecked-api-client.ts b/web/crux/src/app/registry/registry-clients/unchecked-api-client.ts index 34cca40da..df7396724 100644 --- a/web/crux/src/app/registry/registry-clients/unchecked-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/unchecked-api-client.ts @@ -1,13 +1,13 @@ import { CruxBadRequestException } from 'src/exception/crux-exception' -import { RegistryImageTag, RegistryImageTags } from '../registry.message' -import { RegistryApiClient } from './registry-api-client' +import { RegistryImageWithTags } from '../registry.message' +import { RegistryApiClient, RegistryImageTagInfo } from './registry-api-client' class UncheckedApiClient implements RegistryApiClient { catalog(): Promise { throw new CruxBadRequestException({ message: 'Unchecked registries have no catalog API!' }) } - tags(): Promise { + tags(): Promise { throw new CruxBadRequestException({ message: 'Unchecked registries have no tags API!' }) } @@ -15,7 +15,7 @@ class UncheckedApiClient implements RegistryApiClient { return {} } - async tagInfo(image: string, tag: string): Promise { + async tagInfo(): Promise { return null } } diff --git a/web/crux/src/app/registry/registry-clients/v2-http-api-client.ts b/web/crux/src/app/registry/registry-clients/v2-http-api-client.ts index 47d1ca851..1a61ae332 100644 --- a/web/crux/src/app/registry/registry-clients/v2-http-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/v2-http-api-client.ts @@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common' import { Cache } from 'cache-manager' import { CruxInternalServerErrorException } from 'src/exception/crux-exception' import { USER_AGENT_CRUX } from 'src/shared/const' -import { RegistryImageTag } from '../registry.message' +import { RegistryImageTagInfo } from './registry-api-client' type V2Error = { code: string @@ -425,7 +425,7 @@ export default class V2HttpApiClient { return this.fetchLabelsByManifest(image, tagManifest, 0) } - async fetchTagInfo(image: string, tag: string): Promise { + async fetchTagInfo(image: string, tag: string): Promise { const tagManifest = await this.fetchTagManifest(image, tag) if (!tagManifest) { return null diff --git a/web/crux/src/app/registry/registry-clients/v2-registry-api-client.ts b/web/crux/src/app/registry/registry-clients/v2-registry-api-client.ts index 0223ab1e2..050c6258c 100644 --- a/web/crux/src/app/registry/registry-clients/v2-registry-api-client.ts +++ b/web/crux/src/app/registry/registry-clients/v2-registry-api-client.ts @@ -1,8 +1,8 @@ import { Cache } from 'cache-manager' import { CruxUnauthorizedException } from 'src/exception/crux-exception' import { getRegistryApiException } from 'src/exception/registry-exception' -import { RegistryImageTag, RegistryImageTags } from '../registry.message' -import { RegistryApiClient, fetchInfoForTags } from './registry-api-client' +import { RegistryImageWithTags } from '../registry.message' +import { RegistryApiClient, RegistryImageTagInfo, fetchInfoForTags } from './registry-api-client' import V2HttpApiClient from './v2-http-api-client' export type RegistryV2ApiClientOptions = { @@ -70,7 +70,7 @@ class RegistryV2ApiClient implements RegistryApiClient { return repositories.filter(it => it.includes(text)) } - async tags(image: string): Promise { + async tags(image: string): Promise { const res = await RegistryV2ApiClient.fetchPaginatedEndpoint(it => this.fetch(it), `/${image}/tags/list`) if (!res.ok) { throw getRegistryApiException(res, 'Tags request') @@ -104,7 +104,7 @@ class RegistryV2ApiClient implements RegistryApiClient { return this.createApiClient().fetchLabels(image, tag) } - async tagInfo(image: string, tag: string): Promise { + async tagInfo(image: string, tag: string): Promise { return this.createApiClient().fetchTagInfo(image, tag) } diff --git a/web/crux/src/app/registry/registry.message.ts b/web/crux/src/app/registry/registry.message.ts index 498dd29c6..56334c3af 100644 --- a/web/crux/src/app/registry/registry.message.ts +++ b/web/crux/src/app/registry/registry.message.ts @@ -1,12 +1,14 @@ +export type FindImageResult = { + name: string +} + +export const WS_TYPE_FIND_IMAGE = 'find-image' export type FindImageMessage = { registryId: string filter: string } -export type FindImageResult = { - name: string -} - +export const WS_TYPE_FIND_IMAGE_RESULT = 'find-image-result' export type FindImageResultMessage = { registryId: string images: FindImageResult[] @@ -17,18 +19,21 @@ export type RegistryImages = { images: string[] } +export const WS_TYPE_FETCH_IMAGE_TAGS = 'fetch-image-tags' export type FetchImageTagsMessage = RegistryImages export type RegistryImageTag = { - created: string + name: string + created?: string } -export type RegistryImageTags = { +export type RegistryImageWithTags = { name: string - tags: Record + tags: RegistryImageTag[] } +export const WS_TYPE_REGISTRY_IMAGE_TAGS = 'registry-image-tags' export type RegistryImageTagsMessage = { registryId: string - images: RegistryImageTags[] + images: RegistryImageWithTags[] } diff --git a/web/crux/src/app/registry/registry.ws.gateway.ts b/web/crux/src/app/registry/registry.ws.gateway.ts index 341bb1828..d549b2353 100644 --- a/web/crux/src/app/registry/registry.ws.gateway.ts +++ b/web/crux/src/app/registry/registry.ws.gateway.ts @@ -14,6 +14,10 @@ import { FindImageMessage, FindImageResultMessage, RegistryImageTagsMessage, + WS_TYPE_FETCH_IMAGE_TAGS, + WS_TYPE_FIND_IMAGE, + WS_TYPE_FIND_IMAGE_RESULT, + WS_TYPE_REGISTRY_IMAGE_TAGS, } from './registry.message' const TeamSlug = () => WsParam('teamSlug') @@ -38,7 +42,7 @@ export default class RegistryWebSocketGateway { } @AuditLogLevel('disabled') - @SubscribeMessage('find-image') + @SubscribeMessage(WS_TYPE_FIND_IMAGE) async findImage( @TeamSlug() teamSlug: string, @SocketMessage() message: FindImageMessage, @@ -51,7 +55,7 @@ export default class RegistryWebSocketGateway { const images = await api.client.catalog(message.filter) return { - type: 'find-image-result', + type: WS_TYPE_FIND_IMAGE_RESULT, data: { registryId: message.registryId, images: images.map(it => ({ @@ -64,7 +68,7 @@ export default class RegistryWebSocketGateway { } @AuditLogLevel('disabled') - @SubscribeMessage('fetch-image-tags') + @SubscribeMessage(WS_TYPE_FETCH_IMAGE_TAGS) async fetchImageTags( @TeamSlug() teamSlug: string, @SocketMessage() message: FetchImageTagsMessage, @@ -77,7 +81,7 @@ export default class RegistryWebSocketGateway { const tags = message.images.map(it => api.client.tags(it)) return { - type: 'registry-image-tags', + type: WS_TYPE_REGISTRY_IMAGE_TAGS, data: { registryId: message.registryId, images: await Promise.all(tags),