From 07499deac1a65027933d74ee1e815a6f5ecf8c30 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 11 Feb 2025 20:01:29 +0100 Subject: [PATCH] feat: edge images view (#1886) --- .../cd/services/service/ServiceSecrets.tsx | 2 +- assets/src/components/edge/hooks.ts | 2 +- assets/src/components/edge/images/Images.tsx | 154 ++++++++---------- assets/src/components/edge/images/hooks.ts | 106 ------------ assets/src/generated/graphql.ts | 78 +++++++++ assets/src/graph/edge.graphql | 30 ++++ 6 files changed, 177 insertions(+), 195 deletions(-) delete mode 100644 assets/src/components/edge/images/hooks.ts diff --git a/assets/src/components/cd/services/service/ServiceSecrets.tsx b/assets/src/components/cd/services/service/ServiceSecrets.tsx index d551591d26..471dd52bb3 100644 --- a/assets/src/components/cd/services/service/ServiceSecrets.tsx +++ b/assets/src/components/cd/services/service/ServiceSecrets.tsx @@ -280,7 +280,7 @@ const SecretValueSC = styled.div(({ theme }) => ({ }, })) -function SecretValue({ children }: { children: string }) { +export function SecretValue({ children }: { children: string }) { const [reveal, setReveal] = useState(false) return ( diff --git a/assets/src/components/edge/hooks.ts b/assets/src/components/edge/hooks.ts index cb67e74354..803cc22c1b 100644 --- a/assets/src/components/edge/hooks.ts +++ b/assets/src/components/edge/hooks.ts @@ -19,7 +19,7 @@ function useDirectory({ filtered = true }: { filtered?: boolean } = {}) { { path: EDGE_IMAGES_REL_PATH, label: 'Images', - enabled: false, + enabled: true, }, ] diff --git a/assets/src/components/edge/images/Images.tsx b/assets/src/components/edge/images/Images.tsx index 2579d18799..1dd261c5a1 100644 --- a/assets/src/components/edge/images/Images.tsx +++ b/assets/src/components/edge/images/Images.tsx @@ -2,121 +2,101 @@ import { LoopingLogo, Table } from '@pluralsh/design-system' import { createColumnHelper } from '@tanstack/react-table' import { ReactNode } from 'react' import { - Types_CustomResourceObject, - useCustomResourcesQuery, -} from '../../../generated/graphql-kubernetes.ts' -import { KubernetesClient } from '../../../helpers/kubernetes.client.ts' + ClusterIsoImageEdge, + useClusterIsoImagesQuery, +} from '../../../generated/graphql.ts' +import { ObscuredToken } from '../../profile/ObscuredToken.tsx' import { GqlError } from '../../utils/Alert.tsx' -import { DEFAULT_REACT_VIRTUAL_OPTIONS } from '../../utils/table/useFetchPaginatedData.tsx' +import CopyButton from '../../utils/CopyButton.tsx' +import { DateTimeCol } from '../../utils/table/DateTimeCol.tsx' import { - OSArtifact, - useDescribeOSArtifact, - useGetManagementCluster, -} from './hooks.ts' + DEFAULT_REACT_VIRTUAL_OPTIONS, + useFetchPaginatedData, +} from '../../utils/table/useFetchPaginatedData.tsx' -const OS_ARTIFACT_CRD_NAME = 'osartifacts.build.kairos.io' -const ALL_NAMESPACES = ' ' - -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() const columns = [ - columnHelper.accessor((artifact) => artifact, { + columnHelper.accessor((edge) => edge?.node, { id: 'image', header: 'Image', - meta: { truncate: true, gridTemplate: 'minmax(150px,1fr)' }, - cell: ({ getValue }) => { - const artifact = getValue() - - return ( - <> - {artifact.spec.outputImage.repository}:{artifact.spec.outputImage.tag} - - ) - }, + meta: { truncate: true, gridTemplate: 'minmax(150px, 1fr)' }, + cell: ({ getValue }) => getValue()?.image, }), - columnHelper.accessor((artifact) => artifact.spec.outputImage.registry, { + columnHelper.accessor((edge) => edge?.node, { id: 'registry', header: 'Registry', - meta: { truncate: true, gridTemplate: 'minmax(150px,1fr)' }, - cell: ({ getValue }) => getValue(), + meta: { truncate: true, gridTemplate: 'minmax(150px, 1fr)' }, + cell: ({ getValue }) => getValue()?.registry, }), - columnHelper.accessor((artifact) => artifact.spec.outputImage.username, { + columnHelper.accessor((edge) => edge?.node, { id: 'user', header: 'SSH User', - meta: { truncate: true, gridTemplate: 'minmax(100px,.5fr)' }, - cell: ({ getValue }) => getValue(), + meta: { truncate: true, gridTemplate: 'minmax(125px, .75fr)' }, + cell: ({ getValue }) => getValue()?.user, }), - // columnHelper.accessor((artifact) => artifact, { - // id: 'credentials', - // header: 'Credentials', - // meta: { truncate: true, gridTemplate: 'minmax(150px, .25fr)' }, - // cell: ({ getValue, table }) => { - // const artifact = getValue() - // const { cluster } = table.options.meta as { - // cluster: Cluster - // } - // - // return ( - // - // ) - // }, - // }), - columnHelper.accessor((artifact) => artifact, { - id: 'status', - header: 'Status', - meta: { truncate: true, gridTemplate: '100px' }, - cell: ({ getValue }) => { - const artifact = getValue() - - return <>{artifact.status.phase} - }, + columnHelper.accessor((edge) => edge?.node, { + id: 'password', + header: 'SSH Password', + meta: { gridTemplate: 'minmax(125px, .75fr)' }, + cell: ({ getValue }) => ( +
+ + +
+ ), + }), + columnHelper.accessor((edge) => edge?.node?.project, { + id: 'project', + header: 'Project', + meta: { truncate: true, gridTemplate: 'minmax(100px, .5fr)' }, + cell: ({ getValue }) => getValue()?.name, + }), + columnHelper.accessor((edge) => edge?.node?.insertedAt, { + id: 'created', + header: 'Created', + meta: { truncate: true, gridTemplate: 'minmax(100px, .5fr)' }, + cell: ({ getValue }) => , }), ] export default function Images(): ReactNode { - const { cluster } = useGetManagementCluster() - const { data: artifacts, error: artifactsListError } = - useCustomResourcesQuery({ - client: KubernetesClient(cluster?.id ?? ''), - skip: !cluster?.id, - variables: { name: OS_ARTIFACT_CRD_NAME, namespace: ALL_NAMESPACES }, + const { data, error, loading, pageInfo, fetchNextPage, setVirtualSlice } = + useFetchPaginatedData({ pollInterval: 30_000, + queryHook: useClusterIsoImagesQuery, + keyPath: ['clusterIsoImages'], }) - const { items, error: describeError } = useDescribeOSArtifact({ - clusterId: cluster?.id, - artifacts: (artifacts?.handleGetCustomResourceObjectList?.items ?? - []) as Array, - }) - - if (artifactsListError) return - if (describeError) return - if (!artifacts || !items) return + if (error) return + if (!data) return return ( ) ?? []} columns={columns} - reactTableOptions={{ meta: { cluster } }} reactVirtualOptions={DEFAULT_REACT_VIRTUAL_OPTIONS} - data={items} + hasNextPage={pageInfo?.hasNextPage} + fetchNextPage={fetchNextPage} + isFetchingNextPage={loading} + onVirtualSliceChange={setVirtualSlice} virtualizeRows + emptyStateProps={{ + message: 'No images found', + }} /> ) } diff --git a/assets/src/components/edge/images/hooks.ts b/assets/src/components/edge/images/hooks.ts deleted file mode 100644 index b502c96e28..0000000000 --- a/assets/src/components/edge/images/hooks.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { ApolloError } from '@apollo/client' -import { useEffect, useState } from 'react' -import { - NamespacedResourceDocument, - Types_CustomResourceObject, -} from '../../../generated/graphql-kubernetes.ts' -import { - Cluster, - Metadata, - useClusterQuery, -} from '../../../generated/graphql.ts' -import { KubernetesClient } from '../../../helpers/kubernetes.client.ts' - -const useGetManagementCluster = () => { - const { data, error, loading } = useClusterQuery({ - variables: { handle: 'mgmt' }, - }) - - return { cluster: data?.cluster as Cluster, error: error, loading: loading } -} - -interface DescribeOSArtifactsProps { - clusterId: string - artifacts: Array -} - -interface OSArtifact { - apiVersion: string - kind: string - metadata: Metadata - spec: OSArtifactSpec - status: { - conditions: Array<{ - type: 'Ready' | string - status: 'True' | 'False' - reason: string - message: string - }> - phase: string - } -} - -interface OSArtifactSpec { - cloudConfigRef: { key: string; name: string } - fileBundles: Record - imageName: string - model: string - outputImage: { - registry: string - repository: string - tag: string - username: string - passwordSecretKeyRef: { key: string; name: string } - } -} - -const useDescribeOSArtifact = ({ - clusterId, - artifacts, -}: DescribeOSArtifactsProps) => { - const [items, setItems] = useState>([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - const fetchData = async () => { - const fetchedArtifacts: Array = [] - const client = KubernetesClient(clusterId ?? '') - - if (!client) { - setLoading(false) - return - } - - for (const artifact of artifacts) { - const { data, error } = await client.query({ - query: NamespacedResourceDocument, - variables: { - name: artifact.objectMeta.name, - namespace: artifact.objectMeta.namespace, - kind: artifact.typeMeta.kind, - }, - }) - - if (error) { - setError(error) - break - } - - fetchedArtifacts.push(data?.handleGetResource?.Object) - } - - setLoading(false) - setItems(fetchedArtifacts) - } - - if (artifacts?.length > 0) { - fetchData() - } - }, [artifacts, clusterId]) - - return { items, loading, error } -} - -export type { OSArtifact } -export { useGetManagementCluster, useDescribeOSArtifact } diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index a53168e4c0..6ce13a4558 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -10985,6 +10985,8 @@ export type ClusterRegistrationFragment = { __typename?: 'ClusterRegistration', export type TagFragment = { __typename?: 'Tag', name: string, value: string }; +export type IsoImageFragment = { __typename?: 'ClusterIsoImage', id: string, user?: string | null, password?: string | null, registry: string, image: string, insertedAt?: string | null, project?: { __typename?: 'Project', name: string } | null }; + export type ClusterRegistrationQueryVariables = Exact<{ id?: InputMaybe; machineId?: InputMaybe; @@ -11025,6 +11027,16 @@ export type DeleteClusterRegistrationMutationVariables = Exact<{ export type DeleteClusterRegistrationMutation = { __typename?: 'RootMutationType', deleteClusterRegistration?: { __typename?: 'ClusterRegistration', id: string, insertedAt?: string | null, updatedAt?: string | null, machineId: string, name?: string | null, handle?: string | null, metadata?: Record | null, tags?: Array<{ __typename?: 'Tag', name: string, value: string } | null> | null, creator?: { __typename?: 'User', name: string, email: string, profile?: string | null } | null, project?: { __typename?: 'Project', id: string, name: string, default?: boolean | null, description?: string | null } | null } | null }; +export type ClusterIsoImagesQueryVariables = Exact<{ + after?: InputMaybe; + first?: InputMaybe; + before?: InputMaybe; + last?: InputMaybe; +}>; + + +export type ClusterIsoImagesQuery = { __typename?: 'RootQueryType', clusterIsoImages?: { __typename?: 'ClusterIsoImageConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ClusterIsoImageEdge', node?: { __typename?: 'ClusterIsoImage', id: string, user?: string | null, password?: string | null, registry: string, image: string, insertedAt?: string | null, project?: { __typename?: 'Project', name: string } | null } | null } | null> | null } | null }; + export type GroupMemberFragment = { __typename?: 'GroupMember', user?: { __typename?: 'User', id: string, pluralId?: string | null, name: string, email: string, profile?: string | null, backgroundColor?: string | null, readTimestamp?: string | null, emailSettings?: { __typename?: 'EmailSettings', digest?: boolean | null } | null, roles?: { __typename?: 'UserRoles', admin?: boolean | null } | null, personas?: Array<{ __typename?: 'Persona', id: string, name: string, description?: string | null, bindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'PersonaConfiguration', all?: boolean | null, deployments?: { __typename?: 'PersonaDeployment', addOns?: boolean | null, clusters?: boolean | null, pipelines?: boolean | null, providers?: boolean | null, repositories?: boolean | null, services?: boolean | null } | null, home?: { __typename?: 'PersonaHome', manager?: boolean | null, security?: boolean | null } | null, sidebar?: { __typename?: 'PersonaSidebar', audits?: boolean | null, kubernetes?: boolean | null, pullRequests?: boolean | null, settings?: boolean | null, backups?: boolean | null, stacks?: boolean | null } | null } | null } | null> | null } | null, group?: { __typename?: 'Group', id: string, name: string, description?: string | null, global?: boolean | null, insertedAt?: string | null, updatedAt?: string | null } | null }; export type GroupFragment = { __typename?: 'Group', id: string, name: string, description?: string | null, global?: boolean | null, insertedAt?: string | null, updatedAt?: string | null }; @@ -13873,6 +13885,19 @@ export const ClusterRegistrationFragmentDoc = gql` ${TagFragmentDoc} ${UserTinyFragmentDoc} ${ProjectTinyFragmentDoc}`; +export const IsoImageFragmentDoc = gql` + fragment IsoImage on ClusterIsoImage { + id + user + password + registry + image + insertedAt + project { + name + } +} + `; export const GroupFragmentDoc = gql` fragment Group on Group { id @@ -21782,6 +21807,57 @@ export function useDeleteClusterRegistrationMutation(baseOptions?: Apollo.Mutati export type DeleteClusterRegistrationMutationHookResult = ReturnType; export type DeleteClusterRegistrationMutationResult = Apollo.MutationResult; export type DeleteClusterRegistrationMutationOptions = Apollo.BaseMutationOptions; +export const ClusterIsoImagesDocument = gql` + query ClusterISOImages($after: String, $first: Int, $before: String, $last: Int) { + clusterIsoImages(after: $after, first: $first, before: $before, last: $last) { + pageInfo { + ...PageInfo + } + edges { + node { + ...IsoImage + } + } + } +} + ${PageInfoFragmentDoc} +${IsoImageFragmentDoc}`; + +/** + * __useClusterIsoImagesQuery__ + * + * To run a query within a React component, call `useClusterIsoImagesQuery` and pass it any options that fit your needs. + * When your component renders, `useClusterIsoImagesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useClusterIsoImagesQuery({ + * variables: { + * after: // value for 'after' + * first: // value for 'first' + * before: // value for 'before' + * last: // value for 'last' + * }, + * }); + */ +export function useClusterIsoImagesQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ClusterIsoImagesDocument, options); + } +export function useClusterIsoImagesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ClusterIsoImagesDocument, options); + } +export function useClusterIsoImagesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(ClusterIsoImagesDocument, options); + } +export type ClusterIsoImagesQueryHookResult = ReturnType; +export type ClusterIsoImagesLazyQueryHookResult = ReturnType; +export type ClusterIsoImagesSuspenseQueryHookResult = ReturnType; +export type ClusterIsoImagesQueryResult = Apollo.QueryResult; export const GroupsDocument = gql` query Groups($q: String, $first: Int = 20, $after: String) { groups(q: $q, first: $first, after: $after) { @@ -26064,6 +26140,7 @@ export const namedOperations = { ClusterUsageScalingRecommendations: 'ClusterUsageScalingRecommendations', ClusterRegistration: 'ClusterRegistration', ClusterRegistrations: 'ClusterRegistrations', + ClusterISOImages: 'ClusterISOImages', Groups: 'Groups', SearchGroups: 'SearchGroups', GroupMembers: 'GroupMembers', @@ -26348,6 +26425,7 @@ export const namedOperations = { ClusterScalingRecommendation: 'ClusterScalingRecommendation', ClusterRegistration: 'ClusterRegistration', Tag: 'Tag', + IsoImage: 'IsoImage', GroupMember: 'GroupMember', Group: 'Group', KubernetesCluster: 'KubernetesCluster', diff --git a/assets/src/graph/edge.graphql b/assets/src/graph/edge.graphql index 443511cfbf..4af1c4b153 100644 --- a/assets/src/graph/edge.graphql +++ b/assets/src/graph/edge.graphql @@ -22,6 +22,18 @@ fragment Tag on Tag { value } +fragment IsoImage on ClusterIsoImage { + id + user + password + registry + image + insertedAt + project { + name + } +} + query ClusterRegistration($id: ID, $machineId: String) { clusterRegistration(id: $id, machineId: $machineId) { ...ClusterRegistration @@ -73,3 +85,21 @@ mutation DeleteClusterRegistration($id: ID!) { ...ClusterRegistration } } + +query ClusterISOImages( + $after: String + $first: Int + $before: String + $last: Int +) { + clusterIsoImages(after: $after, first: $first, before: $before, last: $last) { + pageInfo { + ...PageInfo + } + edges { + node { + ...IsoImage + } + } + } +}