Skip to content

Commit

Permalink
feat: Edge view (#1776)
Browse files Browse the repository at this point in the history
  • Loading branch information
maciaszczykm authored Jan 23, 2025
1 parent cf8fa35 commit 38a69b5
Show file tree
Hide file tree
Showing 12 changed files with 731 additions and 4 deletions.
2 changes: 1 addition & 1 deletion assets/src/components/cd/services/TagSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function TagSelection({
css={{ '&&': { flexGrow: 0 } }}
type="secondary"
tooltip={tagIsValid ? 'Add tag' : 'Tag is incomplete or invalid'}
size="medium"
size="large"
clickable={tagIsValid}
icon={
<PlusIcon
Expand Down
9 changes: 9 additions & 0 deletions assets/src/components/commandpalette/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
WarningShieldIcon,
setThemeColorMode,
useThemeColorMode,
RamIcon,
} from '@pluralsh/design-system'
import { UseHotkeysOptions } from '@saas-ui/use-hotkeys'
import { isEmpty } from 'lodash'
Expand Down Expand Up @@ -52,6 +53,7 @@ import { STACKS_ROOT_PATH } from '../../routes/stacksRoutesConsts'
import { mapExistingNodes } from '../../utils/graphql'
import { useProjectId } from '../contexts/ProjectsContext'
import { useShareSecretOpen } from '../sharesecret/ShareSecretContext'
import { EDGE_ABS_PATH } from '../../routes/edgeRoutes.tsx'

type CommandGroup = {
commands: Command[]
Expand Down Expand Up @@ -169,6 +171,13 @@ export function useCommands(): CommandGroup[] {
deps: [navigate],
hotkeys: ['shift A', '5'],
},
{
label: 'Edge',
icon: RamIcon,
callback: () => navigate(EDGE_ABS_PATH),
deps: [navigate],
hotkeys: ['shift E'],
},
{
label: 'Service catalog',
icon: CatalogIcon,
Expand Down
130 changes: 130 additions & 0 deletions assets/src/components/edge/CompleteClusterRegistrationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Button, FormField, Modal, Input } from '@pluralsh/design-system'
import { ComponentProps, useState } from 'react'
import { useTheme } from 'styled-components'
import { useUpdateClusterRegistrationMutation } from 'generated/graphql'
import { ModalMountTransition } from 'components/utils/ModalMountTransition'
import { GqlError } from '../utils/Alert.tsx'
import { TagSelection } from '../cd/services/TagSelection.tsx'
import { tagsToNameValue } from '../cd/services/CreateGlobalService.tsx'
function CompleteClusterRegistrationModal({
id,
machineId,
open,
onClose,
refetch,
}: {
id: string
machineId: string
open: boolean
onClose: () => void
refetch?: () => void
}) {
const theme = useTheme()
const [name, setName] = useState('')
const [handle, setHandle] = useState('')
const [tags, setTags] = useState<Record<string, string>>({})

const [mutation, { loading, error }] = useUpdateClusterRegistrationMutation({
onCompleted: () => {
onClose()
refetch?.()
},
})

return (
<Modal
onOpenAutoFocus={(e) => e.preventDefault()}
asForm
onSubmit={(e) => {
e.preventDefault()
mutation({
variables: {
id: id,
attributes: {
name,
handle,
tags: tagsToNameValue(tags),
},
},
})
}}
size="large"
open={open}
onClose={onClose}
header={`Complete cluster registration`}
actions={
<div
css={{
display: 'flex',
flexDirection: 'row-reverse',
gap: theme.spacing.small,
}}
>
<Button
loading={loading}
primary
disabled={!name}
type="submit"
>
Complete
</Button>
<Button
secondary
type="button"
onClick={() => onClose?.()}
>
Close
</Button>
</div>
}
>
<div
css={{
display: 'flex',
flexDirection: 'column',
gap: theme.spacing.large,
}}
>
<p>
Provide cluster details to complete registration on machine with{' '}
{machineId} ID.
</p>
{error && <GqlError error={error} />}
<FormField
label="Name"
required
>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
/>
</FormField>
<FormField label="Handle">
<Input
value={handle}
onChange={(e) => setHandle(e.target.value)}
/>
</FormField>
<FormField label="Tags">
<TagSelection
{...{
setTags,
tags,
theme,
}}
/>
</FormField>
</div>
</Modal>
)
}

export function CreateCompleteClusterRegistrationModal(
props: ComponentProps<typeof CompleteClusterRegistrationModal>
) {
return (
<ModalMountTransition open={props.open}>
<CompleteClusterRegistrationModal {...props} />
</ModalMountTransition>
)
}
202 changes: 202 additions & 0 deletions assets/src/components/edge/Edge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { ResponsivePageFullWidth } from 'components/utils/layout/ResponsivePageFullWidth'
import { useTheme } from 'styled-components'
import {
AppIcon,
Button,
ChipList,
LoopingLogo,
Table,
useSetBreadcrumbs,
} from '@pluralsh/design-system'
import {
DEFAULT_REACT_VIRTUAL_OPTIONS,
useFetchPaginatedData,
} from '../utils/table/useFetchPaginatedData.tsx'
import { FullHeightTableWrap } from '../utils/layout/FullHeightTableWrap.tsx'
import {
ClusterRegistrationFragment,
useClusterRegistrationsQuery,
} from '../../generated/graphql.ts'
import { useMemo, useState } from 'react'
import { mapExistingNodes } from '../../utils/graphql.ts'
import { createColumnHelper } from '@tanstack/react-table'
import { DateTimeCol } from '../utils/table/DateTimeCol.tsx'
import { GqlError } from '../utils/Alert.tsx'

import { EDGE_BASE_CRUMBS } from '../../routes/edgeRoutes.tsx'
import { Flex } from 'honorable'
import { StackedText } from '../utils/table/StackedText.tsx'
import { CreateCompleteClusterRegistrationModal } from './CompleteClusterRegistrationModal.tsx'

const renderTag = (tag) => `${tag.name}${tag.value ? `: ${tag.value}` : ''}`

export const columnHelper = createColumnHelper<ClusterRegistrationFragment>()

const columns = [
columnHelper.accessor((registration) => registration.machineId, {
id: 'machineId',
header: 'Machine ID',
meta: { truncate: true, gridTemplate: 'minmax(150px,1fr)' },
cell: ({ getValue }) => getValue(),
}),
columnHelper.accessor((registration) => registration.project?.name, {
id: 'project',
header: 'Project',
meta: { truncate: true, gridTemplate: 'minmax(150px,1fr)' },
}),
columnHelper.accessor(() => null, {
id: 'cluster',
header: 'Cluster',
meta: { truncate: true, gridTemplate: 'minmax(150px,1fr)' },
cell: ({
row: {
original: { name, handle },
},
}) => (
<StackedText
first={name}
second={handle ? `handle: ${handle}` : undefined}
/>
),
}),
columnHelper.accessor((registration) => registration.tags ?? [], {
id: 'tags',
header: 'Tags',
cell: ({ getValue }) => (
<ChipList
size="small"
limit={1}
values={getValue()}
transformValue={renderTag}
tooltip
truncateWidth={150}
emptyState={null}
/>
),
}),
columnHelper.accessor((registration) => registration.creator, {
id: 'creator',
header: 'Creator',
meta: { truncate: true, gridTemplate: 'minmax(150px,1fr)' },
cell: ({ getValue }) => {
const creator = getValue()

return (
<Flex
align="center"
gap="xsmall"
>
{(creator?.profile || creator?.name) && (
<AppIcon
url={creator?.profile ?? undefined}
name={creator?.name}
size="xxsmall"
spacing="none"
/>
)}
{creator?.email}
</Flex>
)
},
}),
columnHelper.accessor((registration) => registration.insertedAt, {
id: 'insertedAt',
header: 'Created',
enableSorting: true,
enableGlobalFilter: true,
cell: ({ getValue }) => <DateTimeCol date={getValue()} />,
}),
columnHelper.accessor(() => null, {
id: 'actions',
header: '',
meta: { gridTemplate: `fit-content(100px)` },
cell: function Cell({
row: {
original: { id, machineId, name },
},
table,
}) {
const { refetch } = table.options.meta as { refetch?: () => void }
const [open, setOpen] = useState(false)

return (
<>
{!name && (
<Button
secondary
small
pulse
onClick={() => setOpen(true)}
>
Complete
</Button>
)}
<CreateCompleteClusterRegistrationModal
id={id}
machineId={machineId}
open={open}
onClose={() => setOpen(false)}
refetch={refetch}
/>
</>
)
},
}),
]

export default function Edge() {
const theme = useTheme()

useSetBreadcrumbs(EDGE_BASE_CRUMBS)

const {
data,
loading,
error,
refetch,
pageInfo,
fetchNextPage,
setVirtualSlice,
} = useFetchPaginatedData({
queryHook: useClusterRegistrationsQuery,
keyPath: ['clusterRegistrations'],
})

const clusterRegistrations = useMemo(
() => mapExistingNodes(data?.clusterRegistrations),
[data?.clusterRegistrations]
)

if (error) return <GqlError error={error} />

if (!data) return <LoopingLogo />

return (
<ResponsivePageFullWidth
scrollable={false}
headingContent={
<div css={{ ...theme.partials.text.subtitle1 }}>
Edge cluster registrations
</div>
}
>
<FullHeightTableWrap>
<Table
columns={columns}
reactTableOptions={{ meta: { refetch } }}
reactVirtualOptions={DEFAULT_REACT_VIRTUAL_OPTIONS}
data={clusterRegistrations}
virtualizeRows
hasNextPage={pageInfo?.hasNextPage}
fetchNextPage={fetchNextPage}
isFetchingNextPage={loading}
onVirtualSliceChange={setVirtualSlice}
css={{
maxHeight: 'unset',
height: '100%',
}}
/>
</FullHeightTableWrap>
</ResponsivePageFullWidth>
)
}
Loading

0 comments on commit 38a69b5

Please sign in to comment.