Skip to content

Commit

Permalink
feat: merge docker-labels
Browse files Browse the repository at this point in the history
  • Loading branch information
robot9706 committed Dec 12, 2024
1 parent c0876dd commit 9d5607c
Show file tree
Hide file tree
Showing 28 changed files with 709 additions and 132 deletions.
3 changes: 3 additions & 0 deletions web/crux-ui/public/sort-alphabetical-asc.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions web/crux-ui/public/sort-alphabetical-desc.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions web/crux-ui/public/sort-date-asc.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions web/crux-ui/public/sort-date-desc.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { DyoInput } from '@app/elements/dyo-input'
import DyoMessage from '@app/elements/dyo-message'
import DyoRadioButton from '@app/elements/dyo-radio-button'
import { TextFilter, textFilterFor, useFilters } from '@app/hooks/use-filters'
import { RegistryImageTag } from '@app/models'
import useTranslation from 'next-translate/useTranslation'
import { useEffect, useState } from 'react'
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'

interface ImageTagInputProps {
disabled?: boolean
Expand All @@ -13,25 +18,55 @@ interface ImageTagInputProps {
}

type ImageTagSelectListProps = ImageTagInputProps & {
tags: string[]
tags: Record<string, RegistryImageTag>
loadingTags: boolean
}

type Entry = [string, RegistryImageTag]

const ImageTagSelectList = (props: ImageTagSelectListProps) => {
const { disabled, tags, selected: propsSelected, onTagSelected } = props
const { disabled, tags, selected: propsSelected, onTagSelected, loadingTags } = props

const { t } = useTranslation('images')

const [selected, setSelected] = useState(propsSelected)
const [sortState, setSortState] = useState<SortState>({
mode: 'date',
dir: 1,
})

const filters = useFilters<string, TextFilter>({
filters: [textFilterFor<string>(it => [it])],
initialData: tags,
const filters = useFilters<Entry, TextFilter>({
filters: [textFilterFor<Entry>(it => [it[0]])],
initialData: Object.entries(tags),
initialFilter: {
text: '',
},
})

useEffect(() => filters.setItems(tags), [filters, tags])
useEffect(() => filters.setItems(Object.entries(tags)), [tags])

const sortedItems = useMemo(() => {
const items = filters.filtered
switch (sortState.mode) {
case 'alphabetic':
return items.sort((a, b) => b[0].localeCompare(a[0]) * sortState.dir)
case 'date':
return items.sort((a, b) => {
const aDate = Date.parse(a[1].created)
const bDate = Date.parse(b[1].created)

return Math.sign(bDate - aDate) * sortState.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 selectedTagIndex = selected ? sortedItems.findIndex(it => it[0] === selected) : -1

return (
<div className="flex flex-col">
Expand All @@ -45,27 +80,43 @@ const ImageTagSelectList = (props: ImageTagSelectListProps) => {
}
/>

<DyoHeading element="h5" className="text-lg text-bright font-bold">
{t('availableTags')}
</DyoHeading>

{selected ? null : <DyoMessage messageType="info" message={t('selectTag')} />}

<div className="flex flex-col max-h-96 overflow-y-auto">
{filters.filtered.map((it, index) => (
<DyoRadioButton
key={`tag-${it}`}
disabled={disabled}
label={it}
checked={it === selected}
onSelect={() => {
setSelected(it)
onTagSelected(it)
}}
qaLabel={`imageTag-${index}`}
/>
))}
<div className="flex">
<DyoHeading element="h5" className="flex-1 text-lg text-bright font-bold">
{t('availableTags')}
</DyoHeading>
<TagSortToggle state={sortState} onStateChange={setSortState} />
</div>

{loadingTags ? (
<LoadingIndicator />
) : (
<>
{selected ? null : <DyoMessage messageType="info" message={t('selectTag')} />}
<div className="flex flex-col max-h-96 overflow-y-auto">
{sortedItems.map((it, index) => (
<DyoRadioButton
key={`tag-${it}`}
disabled={disabled}
label={it[0]}
checked={it[0] === selected}
onSelect={() => {
setSelected(it[0])
onTagSelected(it[0])
}}
qaLabel={`imageTag-${index}`}
labelTemplate={label => (
<>
{isTagNewer(index, selectedTagIndex) && (
<DyoIndicator color="bg-dyo-violet" className="self-center" />
)}
<DyoLabel className="my-auto mx-2">{label}</DyoLabel>
</>
)}
/>
))}
</div>
</>
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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 type SortState = {
mode: SortModesEnum
dir: -1 | 1
}

type TagSortToggleProps = {
className?: string
state: SortState
onStateChange: (state: SortState) => void
}

const SORT_ICONS: Record<SortModesEnum, { '1': string; '-1': string }> = {
alphabetic: {
'1': '/sort-alphabetical-asc.svg',
'-1': '/sort-alphabetical-desc.svg',
},
date: {
'1': '/sort-date-asc.svg',
'-1': '/sort-date-desc.svg',
},
}

const TagSortToggle = (props: TagSortToggleProps) => {
const { className, state, onStateChange } = props
const { mode, dir } = state

const { t } = useTranslation('common')

const onToggleMode = (newMode: SortModesEnum) => {
if (mode === newMode) {
onStateChange({
mode,
dir: dir == 1 ? -1 : 1,
})
} else {
onStateChange({
mode: newMode,
dir,
})
}
}

return (
<div
className={clsx(
className,
'px-1 bg-medium-eased text-white font-semibold rounded cursor-pointer h-10 flex flex-row',
)}
>
{SORT_MODES.map(it => (
<div
key={it}
className={clsx('px-2 py-1.5 my-1 mr-0.5', mode === it && 'bg-dyo-turquoise rounded')}
onClick={() => onToggleMode(it)}
>
<Image src={SORT_ICONS[it][dir]} alt={mode} width={22} height={22} />
</div>
))}
</div>
)
}

export default TagSortToggle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
WS_TYPE_PATCH_RECEIVED,
WS_TYPE_REGISTRY_FETCH_IMAGE_TAGS,
WS_TYPE_REGISTRY_IMAGE_TAGS,
RegistryImageTag,
} from '@app/models'
import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint'
import useTranslation from 'next-translate/useTranslation'
Expand Down Expand Up @@ -126,9 +127,15 @@ const refreshImageTags = (registriesSock: WebSocketClientEndpoint, images: Versi
})
}

export const selectTagsOfImage = (state: VerionState, image: VersionImage): string[] => {
export const selectTagsOfImage = (state: VerionState, image: VersionImage): Record<string, RegistryImageTag> => {
const regImgTags = state.tags[imageTagKey(image.registry.id, image.name)]
return regImgTags ? regImgTags.tags : image.tag ? [image.tag] : []
return regImgTags
? regImgTags.tags
: image.tag
? {
[image.tag]: null,
}
: {}
}

export const useVersionState = (options: VersionStateOptions): [VerionState, VersionActions] => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const VersionViewList = (props: VersionViewListProps) => {
actions.fetchImageTags(it)
}

const imageTags = tagsModalTarget ? selectTagsOfImage(state, tagsModalTarget) : null

return (
<>
<DyoCard className="relative mt-4">
Expand Down Expand Up @@ -144,6 +146,7 @@ const VersionViewList = (props: VersionViewListProps) => {
qaLabel={QA_MODAL_LABEL_IMAGE_TAGS}
>
<EditImageTags
loadingTags={tagsModalTarget.registry.type === 'unchecked' ? false : imageTags == null}
selected={tagsModalTarget?.tag ?? ''}
tags={tagsModalTarget.registry.type === 'unchecked' ? null : selectTagsOfImage(state, tagsModalTarget)}
onTagSelected={it => actions.selectTagForImage(tagsModalTarget, it)}
Expand Down
5 changes: 3 additions & 2 deletions web/crux-ui/src/elements/dyo-radio-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ type DyoRadioButtonProps = {
checked?: boolean
onSelect?: VoidFunction
qaLabel: string
labelTemplate?: (label?: string) => React.ReactNode
}

const DyoRadioButton = (props: DyoRadioButtonProps) => {
const { className, disabled, label, checked, onSelect, qaLabel } = props
const { className, disabled, label, checked, onSelect, qaLabel, labelTemplate } = props

const handleCheckedChange = () => {
if (disabled) {
Expand All @@ -30,7 +31,7 @@ const DyoRadioButton = (props: DyoRadioButtonProps) => {
<input type="radio" checked={checked} onChange={() => handleCheckedChange()} className="hidden" />
</div>

<DyoLabel className="my-auto mx-2">{label}</DyoLabel>
{labelTemplate ? labelTemplate(label) : <DyoLabel className="my-auto mx-2">{label}</DyoLabel>}
</div>
)
}
Expand Down
6 changes: 5 additions & 1 deletion web/crux-ui/src/models/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,13 @@ export type FindImageResultMessage = {
export const WS_TYPE_REGISTRY_FETCH_IMAGE_TAGS = 'fetch-image-tags'
export type FetchImageTagsMessage = RegistryImages

export type RegistryImageTag = {
created: string
}

export type RegistryImageTags = {
name: string
tags: string[]
tags: Record<string, RegistryImageTag>
}

export const WS_TYPE_REGISTRY_IMAGE_TAGS = 'registry-image-tags'
Expand Down
52 changes: 50 additions & 2 deletions web/crux-ui/src/validations/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ const testRules = (
fieldName: string,
) => {
if (rules.length === 0) {
return true
return null
}

const requiredKeys = rules.map(([key]) => key)
Expand Down Expand Up @@ -504,7 +504,55 @@ const testRules = (
return err
}

return true
return null
}

const testEnvironmentRules = (imageLabels: Record<string, string>) => (envs: UniqueKeyValue[]) => {
const rules = parseDyrectorioEnvRules(imageLabels)
if (!rules) {
return null
}

const requiredRules = Object.entries(rules).filter(([, rule]) => rule.required)
const envRules = requiredRules.filter(([_, rule]) => !rule.secret)

return testRules(envRules, envs, 'environment')
}

const testSecretRules = (imageLabels: Record<string, string>) => (secrets: UniqueSecretKeyValue[]) => {
const rules = parseDyrectorioEnvRules(imageLabels)
if (!rules) {
return null
}

const requiredRules = Object.entries(rules).filter(([, rule]) => rule.required)
const secretRules = requiredRules.filter(([_, rule]) => rule.secret)

return testRules(secretRules, secrets, 'secret')
}

const testEnvironmentRules = (imageLabels: Record<string, string>) => (envs: UniqueKeyValue[]) => {
const rules = parseDyrectorioEnvRules(imageLabels)
if (!rules) {
return true
}

const requiredRules = Object.entries(rules).filter(([, rule]) => rule.required)
const envRules = requiredRules.filter(([_, rule]) => !rule.secret)

return testRules(envRules, envs, 'environment')
}

const testSecretRules = (imageLabels: Record<string, string>) => (secrets: UniqueSecretKeyValue[]) => {
const rules = parseDyrectorioEnvRules(imageLabels)
if (!rules) {
return true
}

const requiredRules = Object.entries(rules).filter(([, rule]) => rule.required)
const secretRules = requiredRules.filter(([_, rule]) => rule.secret)

return testRules(secretRules, secrets, 'secret')
}

const testEnvironmentRules = (imageLabels: Record<string, string>) => (envs: UniqueKeyValue[]) => {
Expand Down
Loading

0 comments on commit 9d5607c

Please sign in to comment.