From cf451c67275bffd66c1b0a6015a638fdc581ef38 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 8 Feb 2023 19:06:44 +0100 Subject: [PATCH] feat: Global app navigation (#265) --- .../components/cluster/ContainerStatuses.tsx | 2 - assets/src/components/layout/AppNav.tsx | 219 ++++++++++++++++++ assets/src/components/layout/Subheader.tsx | 8 +- 3 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 assets/src/components/layout/AppNav.tsx diff --git a/assets/src/components/cluster/ContainerStatuses.tsx b/assets/src/components/cluster/ContainerStatuses.tsx index 1d4c714443..7bd7790fb7 100644 --- a/assets/src/components/cluster/ContainerStatuses.tsx +++ b/assets/src/components/cluster/ContainerStatuses.tsx @@ -48,8 +48,6 @@ export const readinessToTooltipColor = { } as const satisfies Record export function ContainerStatuses({ statuses = [] }: {statuses: ContainerStatus[]}) { - console.log(statuses) - return ( {statuses.map(({ name, readiness }) => ( diff --git a/assets/src/components/layout/AppNav.tsx b/assets/src/components/layout/AppNav.tsx new file mode 100644 index 0000000000..343761c6a6 --- /dev/null +++ b/assets/src/components/layout/AppNav.tsx @@ -0,0 +1,219 @@ +import { + AppsIcon, + Card, + Chip, + CloseIcon, + EmptyState, + ErrorIcon, + IconFrame, + Input, + MagnifyingGlassIcon, + StatusIpIcon, + SuccessIcon, +} from '@pluralsh/design-system' +import { appState, getIcon, hasIcons } from 'components/apps/misc' +import { InstallationContext } from 'components/Installations' +import { Layer } from 'grommet' +import sortBy from 'lodash/sortBy' +import { useContext, useMemo, useState } from 'react' +import { Readiness, ReadinessT } from 'utils/status' +import styled from 'styled-components' +import { useNavigate } from 'react-router-dom' +import AppStatus from 'components/apps/AppStatus' +import Fuse from 'fuse.js' +import { isEmpty } from 'lodash' + +function readinessOrder(readiness: ReadinessT) { + switch (readiness) { + case Readiness.Failed: + return 0 + case Readiness.InProgress: + return 1 + case Readiness.Ready: + return 2 + default: + return 3 + } +} + +function StatusIcon({ readiness }: {readiness: ReadinessT}) { + if (!readiness) return null + + switch (readiness) { + case Readiness.Failed: + return ( + } + /> + ) + case Readiness.InProgress: + return ( + } + /> + ) + default: + return ( + } + /> + ) + } +} + +const StatusPanelTopContainer = styled.div(({ theme }) => ({ + backgroundColor: theme.colors['fill-two'], + borderBottom: theme.borders['fill-two'], + padding: theme.spacing.medium, + position: 'sticky', + top: 0, +})) + +const StatusPanelHeaderWrap = styled.div({ + alignItems: 'center', + display: 'flex', + justifyContent: 'space-between', +}) + +const StatusPanelHeader = styled.div({ + fontSize: 18, + fontWeight: 500, + lineHeight: '24px', +}) + +const AppStatusWrap = styled.div<{last: boolean}>(({ theme, last = false }) => ({ + alignItems: 'center', + cursor: 'pointer', + borderBottom: !last ? theme.borders['fill-two'] : undefined, + display: 'flex', + padding: '12px 16px', + '&:hover': { + backgroundColor: theme.colors['fill-two-hover'], + }, +})) + +const AppIcon = styled.img({ + height: 16, + width: 16, +}) + +const AppName = styled.div(({ theme }) => ({ + ...theme.partials.text.body2, + marginLeft: 8, +})) + +const AppVersion = styled.div(({ theme }) => ({ + ...theme.partials.text.caption, + color: theme.colors['text-xlight'], + display: 'flex', + flexGrow: 1, + marginLeft: 8, +})) + +const searchOptions = { + keys: ['app.name'], + threshold: 0.25, + shouldSort: false, +} + +export function StatusPanel({ statuses, onClose }) { + const navigate = useNavigate() + const [query, setQuery] = useState('') + + const apps = useMemo(() => { + if (isEmpty(query)) return statuses.map(({ app }) => app) + + const fuse = new Fuse<{app, readiness}>(statuses, searchOptions) + + return fuse.search(query).map(({ item: { app } }) => app) + }, [query, statuses]) + + return ( + + + + + Apps + } + onClick={e => onClose(e)} + /> + + } + value={query} + onChange={event => setQuery(event.target.value)} + /> + + {apps.map((app, i) => ( + { + onClose() + navigate(`/apps/${app.name}`) + }} + last={i === apps.length - 1} + > + {hasIcons(app) && } + {app.name} + {app.spec?.descriptor?.version && v{app.spec.descriptor.version}} + + + ))} + {isEmpty(apps) && ( + + )} + + + ) +} + +export default function AppNav() { + const [open, setOpen] = useState(false) + const { applications = [] } = useContext(InstallationContext) + + const statuses = useMemo(() => { + const unsorted = applications.map(app => ({ app, ...appState(app) })) + + return sortBy(unsorted, [({ readiness }) => readinessOrder(readiness), 'app.name']) + }, [applications]) + + return ( + <> + } + clickable + onClick={() => setOpen(true)} + size="small" + > + Apps + 0 && statuses[0].readiness} /> + + {open && ( + setOpen(false)} + /> + )} + + ) +} diff --git a/assets/src/components/layout/Subheader.tsx b/assets/src/components/layout/Subheader.tsx index 7781936f76..08fd104135 100644 --- a/assets/src/components/layout/Subheader.tsx +++ b/assets/src/components/layout/Subheader.tsx @@ -13,6 +13,7 @@ import { ResponsiveLayoutSidenavContainer } from '../utils/layout/ResponsiveLayo import { ResponsiveLayoutSpacer } from '../utils/layout/ResponsiveLayoutSpacer' import { Breadcrumbs } from './Breadcrumbs' +import AppNav from './AppNav' export default function Subheader() { const navigate = useNavigate() @@ -23,11 +24,11 @@ export default function Subheader() { backgroundColor={theme.colors?.grey[950]} borderBottom="1px solid border" minHeight={48} + paddingHorizontal="large" > - + + + + )