Skip to content

Commit

Permalink
feat: cleaner summaries
Browse files Browse the repository at this point in the history
  • Loading branch information
mjaquiery committed Jul 17, 2024
1 parent 63dd8d9 commit 969d6cd
Show file tree
Hide file tree
Showing 17 changed files with 480 additions and 150 deletions.
153 changes: 99 additions & 54 deletions src/Components/ResourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,19 @@ import {
} from "./TypeValueNotation";
import Typography from "@mui/material/Typography";
import {Theme} from "@mui/material/styles";
import ResourceStatuses from "./ResourceStatuses";
import AuthImage from "./AuthImage";
import {Axios} from "./FetchResourceContext";
import ArbitraryFileSummary from "./summaries/ArbitraryFileSummary";
import AdditionalStorageSummary from "./summaries/AdditionalStorageSummary";
import HarvesterSummary from "./summaries/HarvesterSummary";
import TeamSummary from "./summaries/TeamSummary";
import LabSummary from "./summaries/LabSummary";
import UnitSummary from "./summaries/UnitSummary";
import ColumnSummary from "./summaries/ColumnSummary";
import PathSummary from "./summaries/PathSummary";
import FileSummary from "./summaries/FileSummary";
import CyclerTestSummary from "./summaries/CyclerTestSummary";
import ExperimentSummary from "./summaries/ExperimentSummary";

export type Permissions = { read?: boolean, write?: boolean, create?: boolean, destroy?: boolean }
type child_keys = "cells"|"equipment"|"schedules"
Expand Down Expand Up @@ -95,6 +105,93 @@ function PropertiesDivider({children, ...props}: PropsWithChildren<DividerProps>
</Divider>
}

/**
* Resources with custom summaries.
*/
const CUSTOM_SUMMARIES: Partial<Record<LookupKey, (resource: {resource: BaseResource}) => ReactNode>> = {
[LOOKUP_KEYS.ARBITRARY_FILE]: ArbitraryFileSummary,
[LOOKUP_KEYS.ADDITIONAL_STORAGE]: AdditionalStorageSummary,
[LOOKUP_KEYS.HARVESTER]: HarvesterSummary,
[LOOKUP_KEYS.TEAM]: TeamSummary,
[LOOKUP_KEYS.LAB]: LabSummary,
[LOOKUP_KEYS.UNIT]: UnitSummary,
[LOOKUP_KEYS.COLUMN_FAMILY]: ColumnSummary,
[LOOKUP_KEYS.PATH]: PathSummary,
[LOOKUP_KEYS.FILE]: FileSummary,
[LOOKUP_KEYS.CYCLER_TEST]: CyclerTestSummary,
[LOOKUP_KEYS.EXPERIMENT]: ExperimentSummary,
} as const

/**
* Present summary information for a resource.
* If there's a specific summary component, use that.
* Otherwise, pull out fields with PRIORITY_LEVELS.SUMMARY and display them.
*/
function Summary<T extends BaseResource>({apiResource, lookup_key}: {apiResource?: T, lookup_key: LookupKey}) {
if (apiResource === undefined)
return null

if (Object.keys(CUSTOM_SUMMARIES).includes(lookup_key)) {
const COMPONENT = CUSTOM_SUMMARIES[lookup_key as keyof typeof CUSTOM_SUMMARIES]!
return <COMPONENT resource={apiResource} />
}

const is_family_child = (child_key: LookupKey, family_key: LookupKey) => {
if (!get_is_family(family_key)) return false
if (!get_has_family(child_key)) return false
return CHILD_LOOKUP_KEYS[family_key] === child_key
}

const summarise = (
data: Serializable,
many: boolean,
key: string,
lookup?: LookupKey|AutocompleteKey
): ReactNode => {
if (!data || data instanceof Array && data.length === 0)
return <Typography variant="body2">None</Typography>
if (many) {
const preview_count = 3
const items = data instanceof Array && data.length > preview_count?
data.slice(0, preview_count) : data
return <Grid container sx={{alignItems: "center"}}>
{
items instanceof Array ?
items.map((d, i) => <Grid key={i}>{summarise(d, false, key, lookup)}</Grid>) :
<Grid>{summarise(data, false, key, lookup)}</Grid>
}
{
data instanceof Array && data.length > preview_count && <Grid>+ {data.length - preview_count} more</Grid>
}
</Grid>
}
const field = key? FIELDS[lookup_key] : undefined
const field_info = field? field[key as keyof typeof field] : undefined
return lookup && is_lookup_key(lookup) ?
<ResourceChip
resource_id={id_from_ref_props<string>(data as string | number)}
lookup_key={lookup}
short_name={is_family_child(lookup, lookup_key)}
/> : <Prettify target={to_type_value_notation(data, field_info)}/>
}

return <>
{lookup_key === LOOKUP_KEYS.FILE && apiResource?.has_required_columns && apiResource.png &&
<Stack spacing={2}>
<AuthImage file={apiResource as unknown as {id: string, path: string, png: string}} />
</Stack>
}
{apiResource && <Grid container spacing={1}>{
Object.entries(FIELDS[lookup_key])
.filter((e) => e[1].priority === PRIORITY_LEVELS.SUMMARY)
.map(([k, v]) => <Grid key={k} container xs={12} sx={{alignItems: "center"}}>
<Grid xs={2} lg={1}><Typography variant="subtitle2">{k.replace(/_/g, ' ')}</Typography></Grid>
<Grid xs={10} lg={11}>{summarise(apiResource[k], v.many, k, type_to_key(v.type))}</Grid>
</Grid>)
}</Grid>}
</>
}

function ResourceCard<T extends BaseResource>(
{
resource_id,
Expand Down Expand Up @@ -366,59 +463,8 @@ The file will be added to the Harvester's usual queue for processing.
</Stack>
</CardContent>

const is_family_child = (child_key: LookupKey, family_key: LookupKey) => {
if (!get_is_family(family_key)) return false
if (!get_has_family(child_key)) return false
return CHILD_LOOKUP_KEYS[family_key] === child_key
}

const summarise = (
data: Serializable,
many: boolean,
key: string,
lookup?: LookupKey|AutocompleteKey
): ReactNode => {
if (!data || data instanceof Array && data.length === 0)
return <Typography variant="body2">None</Typography>
if (many) {
const preview_count = 3
const items = data instanceof Array && data.length > preview_count?
data.slice(0, preview_count) : data
return <Grid container sx={{alignItems: "center"}}>
{
items instanceof Array ?
items.map((d, i) => <Grid key={i}>{summarise(d, false, key, lookup)}</Grid>) :
<Grid>{summarise(data, false, key, lookup)}</Grid>
}
{
data instanceof Array && data.length > preview_count && <Grid>+ {data.length - preview_count} more</Grid>
}
</Grid>
}
const field = key? FIELDS[lookup_key] : undefined
const field_info = field? field[key as keyof typeof field] : undefined
return lookup && is_lookup_key(lookup) ?
<ResourceChip
resource_id={id_from_ref_props<string>(data as string | number)}
lookup_key={lookup}
short_name={is_family_child(lookup, lookup_key)}
/> : <Prettify target={to_type_value_notation(data, field_info)}/>
}

const cardSummary = <CardContent>
{lookup_key === LOOKUP_KEYS.FILE && apiResource?.has_required_columns && apiResource.png &&
<Stack spacing={2}>
<AuthImage file={apiResource as unknown as {id: string, path: string, png: string}} />
</Stack>
}
{apiResource && <Grid container spacing={1}>{
Object.entries(FIELDS[lookup_key])
.filter((e) => e[1].priority === PRIORITY_LEVELS.SUMMARY)
.map(([k, v]) => <Grid key={k} container xs={12} sx={{alignItems: "center"}}>
<Grid xs={2} lg={1}><Typography variant="subtitle2">{k.replace(/_/g, ' ')}</Typography></Grid>
<Grid xs={10} lg={11}>{summarise(apiResource[k], v.many, k, type_to_key(v.type))}</Grid>
</Grid>)
}</Grid>}
<Summary apiResource={apiResource} lookup_key={lookup_key} />
</CardContent>

const forkModal = passesFilters({apiResource, family}, lookup_key) &&
Expand Down Expand Up @@ -482,7 +528,6 @@ The file will be added to the Harvester's usual queue for processing.
/>
{isExpanded? cardBody : cardSummary}
{forkModal}
<ResourceStatuses lookup_key={lookup_key}/>
</Card>

const getErrorBody: QueryDependentElement = (queries) => <ErrorCard
Expand Down
8 changes: 8 additions & 0 deletions src/Components/misc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ export function deep_copy<T extends Serializable>(obj: T): T {
export function is_axios_error(error: unknown): error is AxiosError {
return error instanceof Object && 'isAxiosError' in error && error.isAxiosError === true
}

export function humanize_bytes(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
if (bytes <= 0) return '0 Bytes'
if (bytes === 1) return '1 Bytes'
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`
}
29 changes: 29 additions & 0 deletions src/Components/summaries/AdditionalStorageSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {BaseResource} from "../ResourceCard";
import {AdditionalS3StorageType} from "@galv/galv";
import useStyles from "../../styles/UseStyles";
import clsx from "clsx";
import Stack from "@mui/material/Stack";
import Alert from "@mui/material/Alert";
import {humanize_bytes} from "../misc";
import {ReactNode} from "react";
import Typography from "@mui/material/Typography";

export default function AdditionalStorageSummary({ resource } : { resource: BaseResource}) {
const {classes} = useStyles();
const r = resource as unknown as AdditionalS3StorageType

let usage: ReactNode
const usage_text = `Used ${humanize_bytes(r.bytes_used)} of ${humanize_bytes(r.quota_bytes)}`
if (r.bytes_used > r.quota_bytes) {
usage = <Alert severity="error">{usage_text}</Alert>
} else if (r.bytes_used > r.quota_bytes * 0.9) {
usage = <Alert severity="warning">{usage_text}</Alert>
} else {
usage = <Typography>{usage_text}</Typography>
}

return <Stack className={clsx(classes.resourceSummary)} spacing={1}>
{!r.enabled && <Alert severity="warning">This storage is disabled</Alert>}
{usage}
</Stack>
}
16 changes: 16 additions & 0 deletions src/Components/summaries/ArbitraryFileSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {BaseResource} from "../ResourceCard";
import {ArbitraryFile} from "@galv/galv";
import useStyles from "../../styles/UseStyles";
import clsx from "clsx";
import Stack from "@mui/material/Stack";
import Prettify from "../prettify/Prettify";
import Typography from "@mui/material/Typography";

export default function ArbitraryFileSummary({ resource } : { resource: BaseResource}) {
const {classes} = useStyles();
const r = resource as unknown as ArbitraryFile
return <Stack className={clsx(classes.resourceSummary)} spacing={1}>
<Typography variant="body2">{r.description}</Typography>
<Prettify target={{_type: "attachment", _value: r.file}} />
</Stack>
}
24 changes: 24 additions & 0 deletions src/Components/summaries/ChipList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import useStyles from "../../styles/UseStyles";
import {get_url_components} from "../misc";
import Stack from "@mui/material/Stack";
import clsx from "clsx";
import {ResourceChip} from "../ResourceChip";
import Typography from "@mui/material/Typography";

export default function ChipList({chips, max}: { chips: string[], max?: number }) {
const max_items = max ?? 5

const extra = chips.length - max_items

const {classes} = useStyles();
const components = chips.map(p => get_url_components(p))
.filter((c, i) => i < max_items)
.map((c) =>
c?.resource_id && c?.lookup_key &&
<ResourceChip key={c.resource_id} resource_id={c.resource_id} lookup_key={c.lookup_key}/>)

return <Stack direction="row" className={clsx(classes.chipList)}>
{components.length > 0? components : <em>None</em>}
{extra > 0 && <Typography>... and {extra} more</Typography>}
</Stack>
}
23 changes: 23 additions & 0 deletions src/Components/summaries/ColumnSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {BaseResource} from "../ResourceCard";
import {DataColumnType} from "@galv/galv";
import useStyles from "../../styles/UseStyles";
import clsx from "clsx";
import Stack from "@mui/material/Stack";
import {get_url_components} from "../misc";
import {ResourceChip} from "../ResourceChip";
import Typography from "@mui/material/Typography";


export default function UnitSummary({ resource } : { resource: BaseResource}) {
const {classes} = useStyles();
const r = resource as unknown as DataColumnType

const unit = r.unit? get_url_components(r.unit) : undefined

return <Stack className={clsx(classes.resourceSummary)} spacing={1}>
<Typography variant="body2">{r.description}</Typography>
<Stack direction="row">
{r.data_type} {unit && <ResourceChip resource_id={unit.resource_id} lookup_key={unit.lookup_key}/>}
</Stack>
</Stack>
}
24 changes: 24 additions & 0 deletions src/Components/summaries/CyclerTestSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {BaseResource} from "../ResourceCard";
import {CyclerTest} from "@galv/galv";
import useStyles from "../../styles/UseStyles";
import clsx from "clsx";
import Stack from "@mui/material/Stack";
import ChipList from "./ChipList";
import {get_url_components} from "../misc";
import {ResourceChip} from "../ResourceChip";


export default function CyclerTestSummary({ resource } : { resource: BaseResource}) {
const {classes} = useStyles();
const r = resource as unknown as CyclerTest

const c = r.cell? get_url_components(r.cell) : undefined
const s = r.schedule? get_url_components(r.schedule) : undefined

return <Stack className={clsx(classes.resourceSummary)} spacing={1}>
{c && <ResourceChip resource_id={c.resource_id} lookup_key={c.lookup_key} />}
{s && <ResourceChip resource_id={s.resource_id} lookup_key={s.lookup_key} />}
<Stack direction="row">Equipment: <ChipList chips={r.equipment as string[]} /></Stack>
<Stack direction="row">Files: <ChipList chips={r.files as string[]} /></Stack>
</Stack>
}
15 changes: 15 additions & 0 deletions src/Components/summaries/ExperimentSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {BaseResource} from "../ResourceCard";
import {Experiment} from "@galv/galv";
import useStyles from "../../styles/UseStyles";
import clsx from "clsx";
import Stack from "@mui/material/Stack";
import ChipList from "./ChipList";

export default function CyclerTestSummary({ resource } : { resource: BaseResource}) {
const {classes} = useStyles();
const r = resource as unknown as Experiment

return <Stack className={clsx(classes.resourceSummary)} spacing={1}>
<Stack direction="row">Cycler Tests: <ChipList chips={r.cycler_tests as string[]} /></Stack>
</Stack>
}
Loading

0 comments on commit 969d6cd

Please sign in to comment.