Skip to content

Commit

Permalink
enhancement: Homepage improvements (#1591)
Browse files Browse the repository at this point in the history
  • Loading branch information
maciaszczykm authored Nov 19, 2024
1 parent 592fcfb commit 66b8d5a
Show file tree
Hide file tree
Showing 16 changed files with 368 additions and 234 deletions.
7 changes: 6 additions & 1 deletion assets/src/components/ai/AITable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,24 @@ import { TableProps } from '@pluralsh/design-system/dist/components/Table'

export function AITable({
modal = false,
hidePins = false,
query,
rowData,
...props
}: {
modal?: boolean
hidePins?: boolean
query: FetchPaginatedDataResult<AiPinsQuery | ChatThreadsQuery>
rowData: AiPinFragment[] | ChatThreadTinyFragment[]
} & Omit<TableProps, 'data' | 'columns'>) {
const theme = useTheme()

const reactTableOptions = {
meta: { modal },
meta: { modal, hidePins },
}

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

if (!query.data)
return (
<TableSkeleton
Expand Down Expand Up @@ -106,12 +109,14 @@ const TableRow = columnHelper.accessor((item) => item, {
id: item.id,
},
})

return (
<AITableEntry
item={item}
onClickPin={() => (isPin ? unpinThread() : pinThread())}
pinLoading={pinLoading || unpinLoading}
modal={table.options.meta?.modal}
hidePins={table.options.meta?.hidePins}
/>
)
},
Expand Down
38 changes: 21 additions & 17 deletions assets/src/components/ai/AITableEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,14 @@ export function AITableEntry({
onClickPin,
pinLoading,
modal,
hidePins,
...props
}: {
item: ChatThreadTinyFragment | AiPinFragment
onClickPin?: () => void
pinLoading?: boolean
modal?: boolean | null
hidePins?: boolean | null
} & ComponentProps<'div'>) {
const theme = useTheme()
const { pathname } = useLocation()
Expand Down Expand Up @@ -121,23 +123,25 @@ export function AITableEntry({
<Chip severity={isStale ? 'neutral' : 'success'}>
{isStale ? 'Stale' : 'Active'}
</Chip>
<IconFrame
clickable
onClick={(e) => {
e.stopPropagation()
if (pinLoading) return
onClickPin?.()
}}
icon={
pinLoading ? (
<Spinner />
) : isPin ? (
<PushPinFilledIcon color={theme.colors['icon-info']} />
) : (
<PushPinOutlineIcon />
)
}
/>
{!hidePins && (
<IconFrame
clickable
onClick={(e) => {
e.stopPropagation()
if (pinLoading) return
onClickPin?.()
}}
icon={
pinLoading ? (
<Spinner />
) : isPin ? (
<PushPinFilledIcon color={theme.colors['icon-info']} />
) : (
<PushPinOutlineIcon />
)
}
/>
)}
</>
)}
{isInsight ? (
Expand Down
50 changes: 50 additions & 0 deletions assets/src/components/home/AiThreads.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { HOME_CARD_CONTENT_HEIGHT, HomeCard } from './HomeCard.tsx'
import { AiSparkleOutlineIcon } from '@pluralsh/design-system'
import { AITable } from '../ai/AITable.tsx'
import { useFetchPaginatedData } from '../utils/table/useFetchPaginatedData.tsx'
import {
ChatThreadTinyFragment,
useChatThreadsQuery,
} from '../../generated/graphql.ts'
import { useMemo } from 'react'
import { sortThreadsOrPins } from '../ai/AITableEntry.tsx'
import { AI_ABS_PATH } from '../../routes/aiRoutes.tsx'

export function AiThreads() {
const threadsQuery = useFetchPaginatedData({
queryHook: useChatThreadsQuery,
keyPath: ['chatThreads'],
})

const threads = useMemo(
() =>
threadsQuery.data?.chatThreads?.edges
?.map((edge) => edge?.node)
?.sort(sortThreadsOrPins)
?.filter((thread): thread is ChatThreadTinyFragment =>
Boolean(thread)
) ?? [],
[threadsQuery.data?.chatThreads?.edges]
)

return (
<HomeCard
title="Most recent AI threads"
icon={<AiSparkleOutlineIcon />}
link={AI_ABS_PATH}
noPadding
>
<AITable
query={threadsQuery}
rowData={threads}
hidePins
css={{
border: 'none',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
maxHeight: HOME_CARD_CONTENT_HEIGHT,
}}
/>
</HomeCard>
)
}
61 changes: 29 additions & 32 deletions assets/src/components/home/CustomLegend.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,40 @@
import styled from 'styled-components'
import { useTheme } from 'styled-components'

export function CustomLegend({
data,
}: {
data: { label: string; value?: number; color: string }[]
}) {
const theme = useTheme()
return (
<LegendWrapperSC>
<div>
{data.map((item, index) => (
<LegendItemSC key={index}>
<LegendSymbolSC $color={item.color} />
<LegendTextSC>
{`${item.value ?? ''} `}
{item.label}
</LegendTextSC>
</LegendItemSC>
<div
css={{ display: 'flex', alignItems: 'center' }}
key={index}
>
<div
css={{
backgroundColor: item.color,
borderRadius: '50%',
height: 12,
width: 12,
}}
/>
<div
css={{
display: 'flex',
gap: theme.spacing.small,
justifyContent: 'space-between',
marginLeft: theme.spacing.xsmall,
width: '100%',
}}
>
<div css={{ color: theme.colors['text-light'] }}>{item.label}</div>
<div css={{ color: theme.colors['text'] }}>{item.value}</div>
</div>
</div>
))}
</LegendWrapperSC>
</div>
)
}

const LegendWrapperSC = styled.div(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
gap: theme.spacing.medium,
}))
const LegendItemSC = styled.div({
display: 'flex',
alignItems: 'center',
})
const LegendSymbolSC = styled.div<{ $color: string }>(
({ $color: color, theme }) => ({
backgroundColor: color,
borderRadius: '50%',
height: theme.spacing.xsmall,
width: theme.spacing.xsmall,
})
)
const LegendTextSC = styled.span(({ theme }) => ({
marginLeft: theme.spacing.xsmall,
minWidth: 'fit-content',
}))
61 changes: 32 additions & 29 deletions assets/src/components/home/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
import { Breadcrumb, useSetBreadcrumbs } from '@pluralsh/design-system'
import { ResponsivePageFullWidth } from 'components/utils/layout/ResponsivePageFullWidth'
import styled from 'styled-components'

import { useIsManager, useLogin } from 'components/contexts'

import ConsolePageTitle from 'components/utils/layout/ConsolePageTitle'

import { useTheme } from 'styled-components'
import { useIsManager } from 'components/contexts'
import { ClusterOverviewCard } from './clusteroverview/ClusterOverviewCard'
// import { MonthlyClusterCostsCard } from './MonthlyClusterCostsCard'
import { DeploymentsCard } from './deployments/DeploymentsCard'

import { ConstraintViolationsCard } from './managerview/violations/ConstraintViolationsCard'
import { ConstraintViolationsCard } from './violations/ConstraintViolationsCard'
import { PrCard } from './pullrequests/PrCard'
import { AiThreads } from './AiThreads.tsx'

const breadcrumbs: Breadcrumb[] = [{ label: 'home', url: '/' }]

export default function Home() {
useSetBreadcrumbs(breadcrumbs)
const name = useLogin().me?.name
const theme = useTheme()
const isManager = useIsManager()

useSetBreadcrumbs(breadcrumbs)

return (
<ResponsivePageFullWidth style={{ paddingTop: 0 }}>
<ConsolePageTitle
headingProps={{ title2: false, title1: true }}
heading={`Welcome${name ? `, ${name.split(' ')[0]}` : ''}!`}
/>
<HomeContentWrapperSC>
<ResponsivePageFullWidth maxContentWidth={1440}>
<div
css={{
display: 'flex',
flexDirection: 'column',
gap: theme.spacing.large,
paddingBottom: theme.spacing.large,
}}
>
<ClusterOverviewCard />
<AiThreads />
{isManager && <ConstraintViolationsCard />}
<PrCard />
<DeploymentsCard />
{/* <MonthlyClusterCostsCard /> */}
</HomeContentWrapperSC>
<div
css={{
display: 'flex',
flexDirection: 'column',
gap: theme.spacing.large,

'@media (min-width: 1168px)': {
flexDirection: 'row',
},
}}
>
<PrCard />
<DeploymentsCard />
</div>
</div>
</ResponsivePageFullWidth>
)
}

const HomeContentWrapperSC = styled.div(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing.xlarge,
marginTop: theme.spacing.large,
paddingBottom: theme.spacing.xxlarge,
}))
Loading

0 comments on commit 66b8d5a

Please sign in to comment.