Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demo app - Record detail layout #4745

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
5 changes: 3 additions & 2 deletions sandbox/grommet-app/src/pages/layouts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from "react";
import { Link, Outlet, Route } from 'react-router-dom';
import { List, Page, PageContent, PageHeader } from "grommet";
import { RoutedAnchor } from '../../components';
import { Collection, EmptyState, Form, NavigationalSidebar } from './kinds';
import { Collection, EmptyState, Form, NavigationalSidebar, RecordDetail } from './kinds';

const layouts = [
'Collection',
'Dashboard',
'Detail',
'Record detail',
'Empty state',
'Form',
'Home',
Expand Down Expand Up @@ -58,6 +58,7 @@ const routes = [
<Route key="empty-state" path="empty-state" element={<EmptyState />} />,
<Route key="form" path="/layouts/form" element={<Form />} />,
<Route key="navigational-sidebar" path="navigational-sidebar" element={<NavigationalSidebar />} />,
<Route key="record-detail" path="record-detail" element={<RecordDetail />} />,
];

export { Layouts, routes };
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { Box, NameValueList, NameValuePair, Text } from 'grommet';
import { StatusGoodSmall } from 'grommet-icons';
import { sentenceCase } from '../../../../utils/format';

const groupDetails = {
status: {
value: 'Okay',
render: () => <Box direction="row" gap="xsmall" align="center">
<StatusGoodSmall color='status-ok' />
<Text>Okay</Text>
</Box>
,
},
state: 'Job in progress',
group: 'Server group name',
servers: 2,
baseline: 'SPP 2022.09.04(04 Sep 2022)',
power: 'On',
};

const nameProps = {
width: ['xsmall', 'max-content']
};

export const DetailSummary: React.FC = () => {
return (
<NameValueList nameProps={nameProps}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

locally Im getting a TS error for the nameProps I wanted to check on your side if it was fine or getting same error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I get the error too. I'm leaving it because I think Grommet should update the width type definition for NameValueList as it using Grid underneath and an array with min/max values should be valid. Leaving the error as a reminder that this is something that should be fixed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree I was thinking grommet should be enhanced!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@halocline Can you file a ticket on this in Grommet?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grommet/grommet#7509

one step ahead I put up a PR on this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry thought I had commented that I will put up a PR

{Object.entries(groupDetails).map(([name, value]) => (
<NameValuePair
key={name}
name={sentenceCase(name)}
>
{typeof value === 'object' && 'render' in value ?
value.render() :
sentenceCase(value.toString())}
</NameValuePair>
))}
</NameValueList>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";
import { Box, NameValueList, NameValuePair } from "grommet";
import { sentenceCase } from "../../../../utils/format";
import { statusMap } from "../../../../utils/status";

const iLOSecurity = {
'critical': {
servers: [],
label: 'At risk',
},
'warning': {
servers: [{ id: 'server1', name: 'Server 1', url: '#' }],
label: 'Needs attention',
},
'ok': {
servers: [{ id: 'server2', name: 'Server 2', url: '#' }],
label: 'Okay',
},
'unknown': {
servers: [],
label: 'Unknown',
},
'unsupported': {
servers: [],
label: 'Unsupported',
},
}

const nameProps = {
width: ['xsmall', 'max-content']
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include this in grommet-theme-hpe v6.0.0 as the default?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oooh I like your thinking.

Perhaps we could do something similar for bottom pad on PageHeader. Maybe audit some other "we always seem to apply these props".

};

export const ILOSecurity: React.FC = () => {
return (
<NameValueList nameProps={nameProps}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same TS error here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{Object.entries(iLOSecurity).map(([key, detail]) => (
<NameValuePair
key={key}
name={sentenceCase(detail.label)}
>
<Box direction="row" align="center" gap="small">
{React.cloneElement(statusMap.get(key)!.icon, { color: statusMap.get(key)!.color })}
{detail.servers.length} servers
</Box>
</NameValuePair>
))}
</NameValueList>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import {
Box,
Button,
List,
NameValueList,
NameValuePair,
Text
} from 'grommet';
import { Edit, Trash } from 'grommet-icons';
import { sentenceCase } from "../../../../utils/format";

export const JobList = (
{ jobs }:
{
jobs: {
id: string,
name: string,
[key: string]: any,
}[]
}
) => {
return (
<List
data={jobs}
defaultItemProps={{ pad: { horizontal: 'none', vertical: 'xsmall' } }}
>
{(datum) => (
<Box
key={datum.id}
background="background-contrast"
round="small"
pad={{ left: 'small', right: 'xsmall', vertical: 'small' }}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would "small" all around work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we have an icon only button on the right, the padding in the button + pad on the container end up making the left-right whitespace feel unbalanced. Reducing the right pad to xsmall feels more balanced.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree locally I changed it to small to be all around the same size but it felt off a little so I think even though the right is not the same visually looks better

>
<Box direction="row" justify="between">
<Text>{datum.name}</Text>
<Box direction="row">
<Button icon={<Edit />} size="small" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-01-27 at 9 38 19 PM

The spacing/text hierarchy feels awkward here. Feels like we could use a bit more spacing below "Update firmware"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Fixed.

<Button icon={<Trash />} size="small" />
</Box>
</Box>
<NameValueList
nameProps={{ width: 'max-content' }}
valueProps={{ width: 'medium' }}
>
{Object.entries(datum).map(([key, value]) => (
<NameValuePair key={key} name={sentenceCase(key)}>
{Array.isArray(value) ?
value.map((item) => (<Text key={item}>{item}</Text>)) :
value}
</NameValuePair>
))}
</NameValueList>
</Box>
)
}
</List >
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import { Anchor, Box, List, Paragraph, Text } from 'grommet';

const recentActivities = [
{
title: 'Firmware update',
description: `Firmware update failed. Recommendation: Retry
the firmware update. If the issue persists, open a support case
by clicking on the help icon in the UI and select Compute Ops
Management - Create case (error code: FWE-110).`,
datetime: '2025-01-24T09:58:25',
},
{
title: 'Collect hardware inventory',
description: 'Inventory collection successful',
datetime: '2025-01-21T08:37:33',
},
{
title: 'Collect hardware inventory',
description: 'Inventory collection successful',
datetime: '2025-01-03T18:12:56',
},
{
title: 'Server health',
description: 'Network health changed to OK',
datetime: '2025-01-21T14:20:21',
}, {
title: 'Server health',
description: 'Network health changed to OK',
datetime: '2025-01-21T14:20:21',
}
].sort((a, b) => {
return new Date(b.datetime).getTime() - new Date(a.datetime).getTime();
});

interface ActivityItemProps {
title: string;
description: string;
datetime: string;
}

const ActivityItem: React.FC<ActivityItemProps> = ({ title, description, datetime }) => {
return (
<Box gap="xsmall">
<Text color="text-strong" weight={500}>{title}</Text>
<Paragraph margin="none">{description}</Paragraph>
<Text>{Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(datetime))}</Text>
</Box>
);
}

export const RecentActivity: React.FC = () => {
return (
<Box gap="medium">
<List
data={recentActivities}
defaultItemProps={{ pad: { 'vertical': 'small' } }}
>
{datum => (
<ActivityItem
title={datum.title}
description={datum.description}
datetime={datum.datetime}
/>
)}
</List>
<Anchor label="View all activity" href='#' />
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import { Link } from 'react-router-dom';
import {
Box,
Button,
Grid,
Notification,
Page,
PageContent,
PageHeader,
} from 'grommet';
import { RoutedAnchor } from '../../../../components';
import { Close, Previous } from 'grommet-icons';
import ContentPane from '../../../../components/ContentPane';
import { DetailSummary } from './DetailSummary';
import { ScheduledActions } from './ScheduledActions';
import { ILOSecurity } from './ILOSecurity';
import { RecentActivity } from './RecentActivity';
import { ScheduledJobs } from './ScheduledJobs';

export const RecordDetail: React.FC = () => {
const [scheduledJobs, setScheduledJobs] = React.useState<boolean>(false);
const columns = scheduledJobs ? ['flex', 'auto'] : ['flex', 'medium'];
const rows = ['auto'];

const areas = [
['notification', 'context-pane'],
['details', 'context-pane'],
['scheduled-actions', 'context-pane'],
['iLO-security', 'context-pane'],
['unassigned', 'context-pane'],
];

return (
<Page pad={{ bottom: 'xlarge' }} flex="grow">
<PageContent>
<PageHeader
title="Server group name"
subtitle="2 servers"
parent={<RoutedAnchor as={Link} to="/layouts" label="Layouts" icon={<Previous />} />}
/>
<Grid areas={areas} columns={columns} rows={rows} gap="large">
<Box gridArea="notification">
<Notification
status="info"
message="1 job is scheduled."
actions={[{
label: 'View',
href: '#',
onClick: () => { setScheduledJobs(true) },
}]}
/>
</Box>
<ContentPane
gridArea="details"
heading="Details"
level={2}
actions={undefined}
skeleton={undefined}
>
<DetailSummary />
</ContentPane>
<ContentPane
gridArea="scheduled-actions"
heading="Scheduled actions"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-01-27 at 9 40 48 PM

Getting some overlap here -- not sure if that content pane should have overflow or the page should respond earlier to a single column layout

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made adjustments to improve responsive behavior.

level={2}
contain={true}
actions={undefined}
skeleton={undefined}
>
<ScheduledActions />
</ContentPane>
<ContentPane
gridArea="iLO-security"
heading="iLO security"
level={2}
actions={undefined}
skeleton={undefined}
>
<ILOSecurity />
</ContentPane>
<Box gridArea='context-pane' gap="medium">
{scheduledJobs &&
<ContentPane
heading="Scheduled and pending jobs"
level={2}
actions={
<Button
autoFocus
a11yTitle="You are in a new page region displaying scheduled and pending jobs. Remove this region by pressing Enter."
icon={<Close />}
onClick={() => setScheduledJobs(false)}
/>
}
skeleton={undefined}
animation={["slideLeft", "fadeIn"]}
>
<ScheduledJobs level={2} />
</ContentPane>}
<ContentPane
heading="Recent activity"
level={2}
actions={undefined}
skeleton={undefined}
>
<RecentActivity />
</ContentPane>
</Box>
</Grid>
</PageContent>
</Page>
);
}
Loading