Skip to content

Commit

Permalink
feat: Global app navigation (#265)
Browse files Browse the repository at this point in the history
  • Loading branch information
maciaszczykm authored Feb 8, 2023
1 parent 1949a15 commit cf451c6
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 4 deletions.
2 changes: 0 additions & 2 deletions assets/src/components/cluster/ContainerStatuses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ export const readinessToTooltipColor = {
} as const satisfies Record<ReadinessT, string>

export function ContainerStatuses({ statuses = [] }: {statuses: ContainerStatus[]}) {
console.log(statuses)

return (
<Flex gap="xxxsmall">
{statuses.map(({ name, readiness }) => (
Expand Down
219 changes: 219 additions & 0 deletions assets/src/components/layout/AppNav.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<IconFrame
size="xsmall"
icon={<ErrorIcon color="icon-danger" />}
/>
)
case Readiness.InProgress:
return (
<IconFrame
size="xsmall"
icon={<StatusIpIcon color="icon-warning" />}
/>
)
default:
return (
<IconFrame
size="xsmall"
icon={<SuccessIcon color="icon-success" />}
/>
)
}
}

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<string>('')

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 (
<Layer
plain
onClickOutside={onClose}
position="top-right"
margin={{ top: '105px', right: '24px', bottom: '24px' }}
>
<Card
fillLevel={2}
width={420}
overflow="auto"
position="relative"
>
<StatusPanelTopContainer>
<StatusPanelHeaderWrap>
<StatusPanelHeader>Apps</StatusPanelHeader>
<IconFrame
clickable
icon={<CloseIcon />}
onClick={e => onClose(e)}
/>
</StatusPanelHeaderWrap>
<Input
marginTop="xsmall"
placeholder="Filter applications"
startIcon={<MagnifyingGlassIcon size={14} />}
value={query}
onChange={event => setQuery(event.target.value)}
/>
</StatusPanelTopContainer>
{apps.map((app, i) => (
<AppStatusWrap
onClick={() => {
onClose()
navigate(`/apps/${app.name}`)
}}
last={i === apps.length - 1}
>
{hasIcons(app) && <AppIcon src={getIcon(app)} /> }
<AppName>{app.name}</AppName>
{app.spec?.descriptor?.version && <AppVersion>v{app.spec.descriptor.version}</AppVersion>}
<AppStatus app={app} />
</AppStatusWrap>
))}
{isEmpty(apps) && (
<EmptyState
message="No apps found."
description={`"${query}" did not match any of your installed applications.`}
/>
)}
</Card>
</Layer>
)
}

export default function AppNav() {
const [open, setOpen] = useState<boolean>(false)
const { applications = [] } = useContext<any>(InstallationContext)

const statuses = useMemo(() => {
const unsorted = applications.map(app => ({ app, ...appState(app) }))

return sortBy(unsorted, [({ readiness }) => readinessOrder(readiness), 'app.name'])
}, [applications])

return (
<>
<Chip
backgroundColor={open ? 'fill-one-selected' : 'fill-one'}
icon={<AppsIcon />}
clickable
onClick={() => setOpen(true)}
size="small"
>
Apps
<StatusIcon readiness={statuses.length > 0 && statuses[0].readiness} />
</Chip>
{open && (
<StatusPanel
statuses={statuses}
onClose={() => setOpen(false)}
/>
)}
</>
)
}
8 changes: 6 additions & 2 deletions assets/src/components/layout/Subheader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -23,11 +24,11 @@ export default function Subheader() {
backgroundColor={theme.colors?.grey[950]}
borderBottom="1px solid border"
minHeight={48}
paddingHorizontal="large"
>
<ResponsiveLayoutSidenavContainer
gap="small"
display="flex"
paddingHorizontal="large"
width={240}
>
<IconFrame
Expand All @@ -48,9 +49,12 @@ export default function Subheader() {
/>
</ResponsiveLayoutSidenavContainer>
<ResponsiveLayoutSpacer />
<ResponsiveLayoutContentContainer><Flex align="justify"><Breadcrumbs /></Flex></ResponsiveLayoutContentContainer>
<ResponsiveLayoutContentContainer>
<Breadcrumbs />
</ResponsiveLayoutContentContainer>
<ResponsiveLayoutSidecarContainer />
<ResponsiveLayoutSpacer />
<AppNav />
</Flex>

)
Expand Down

0 comments on commit cf451c6

Please sign in to comment.