Skip to content

Commit

Permalink
Migrate robot table to datagrid (#713)
Browse files Browse the repository at this point in the history
* Add robot table datagrid file in order to migrate html table to datagrid

Signed-off-by: angatupyry <[email protected]>

* Export new react-datagrid-table component

Signed-off-by: angatupyry <[email protected]>

* Use robotDatagridTable component instead of Table

Signed-off-by: angatupyry <[email protected]>

* Remove pagination options

Signed-off-by: angatupyry <[email protected]>

* Use robot table interface and remove pagination options

Signed-off-by: angatupyry <[email protected]>

* Remove paginations options

Signed-off-by: angatupyry <[email protected]>

* Remove robot table and use datagrid instead

Signed-off-by: angatupyry <[email protected]>

* Add rowsPerPageOptions and set it with the same value of pageSize

Signed-off-by: angatupyry <[email protected]>

* Add new component test file

Signed-off-by: angatupyry <[email protected]>

* Add new component storybook file

Signed-off-by: angatupyry <[email protected]>

---------

Signed-off-by: angatupyry <[email protected]>
  • Loading branch information
Angatupyry authored Jun 5, 2023
1 parent 14c4211 commit 161e7e3
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 3 deletions.
6 changes: 3 additions & 3 deletions packages/dashboard/src/components/robots/robots-app.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TableContainer } from '@mui/material';
import { TaskState } from 'api-client';
import React from 'react';
import { RobotTable, RobotTableData } from 'react-components';
import { RobotDataGridTable, RobotTableData } from 'react-components';
import { AppEvents } from '../app-events';
import { createMicroApp } from '../micro-app';
import { RmfAppContext } from '../rmf-app';
Expand All @@ -14,6 +14,7 @@ export const RobotsApp = createMicroApp('Robots', () => {
const [robots, setRobots] = React.useState<Record<string, RobotTableData[]>>({});
const [openRobotSummary, setOpenRobotSummary] = React.useState(false);
const [selectedRobot, setSelectedRobot] = React.useState<RobotTableData>();

React.useEffect(() => {
if (!rmf) {
return;
Expand Down Expand Up @@ -88,15 +89,14 @@ export const RobotsApp = createMicroApp('Robots', () => {

return (
<TableContainer>
<RobotTable
<RobotDataGridTable
robots={Object.values(robots).flatMap((r) => r)}
onRobotClick={(_ev, robot) => {
setOpenRobotSummary(true);
AppEvents.robotSelect.next([robot.fleet, robot.name]);
setSelectedRobot(robot);
}}
/>

{openRobotSummary && selectedRobot && (
<RobotSummary robot={selectedRobot} onClose={() => setOpenRobotSummary(false)} />
)}
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/lib/robots/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './robot-info';
export * from './robot-table';
export * from './robot-table-datagrid';
export * from './utils';
83 changes: 83 additions & 0 deletions packages/react-components/lib/robots/robot-table-datagrid.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Status2 as RobotStatus } from 'api-client';
import React from 'react';
import { RobotTableData } from './robot-table';
import { makeRobot } from './test-utils.spec';
import { RobotDataGridTable } from './robot-table-datagrid';

const allStatuses = Object.values(RobotStatus) as RobotStatus[];

describe('RobotTable', () => {
it('shows all robots', () => {
const robots = [makeRobot({ name: 'test_robot1' }), makeRobot({ name: 'test_robot2' })];
const tableData: RobotTableData[] = robots.map((robot) => ({
fleet: 'test_fleet',
name: robot.name,
}));
const root = render(<RobotDataGridTable robots={tableData} />);
expect(root.getByText('test_robot1')).toBeTruthy();
expect(root.getByText('test_robot2')).toBeTruthy();
});

it('smoke test for different robot status', () => {
const robots = allStatuses.map((status) => makeRobot({ name: `${status}_robot`, status }));
render(
<RobotDataGridTable
robots={robots.map((robot) => ({
fleet: 'test_fleet',
name: robot.name,
status: robot.status,
}))}
/>,
);
});

it('onRobotClick is called when row is clicked', () => {
const onRobotClick = jasmine.createSpy();
const root = render(
<RobotDataGridTable
robots={[{ fleet: 'test_fleet', name: 'test_robot' }]}
onRobotClick={onRobotClick}
/>,
);
const robot = root.getByText('test_robot');
userEvent.click(robot);
expect(onRobotClick).toHaveBeenCalled();
});

it('finish time is shown when it is available', () => {
const root = render(
<RobotDataGridTable
robots={[
{ fleet: 'test_fleet', name: 'test_robot', estFinishTime: 1000, lastUpdateTime: 900 },
]}
/>,
);
// TODO: use a less convoluted test when
// https://github.com/testing-library/react-testing-library/issues/1160
// is resolved.
expect(() =>
root.getByText((_, node) => {
if (!node) {
return false;
}
const hasText = (node) => node.textContent === new Date(1000).toLocaleString();
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every((child) => !hasText(child));
return nodeHasText && childrenDontHaveText;
}),
).not.toThrow();
expect(() =>
root.getByText((_, node) => {
if (!node) {
return false;
}
const hasText = (node) => node.textContent === new Date(900).toLocaleString();
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every((child) => !hasText(child));
return nodeHasText && childrenDontHaveText;
}),
).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { RobotDataGridTable } from './robot-table-datagrid';
import { Status2 as RobotStatus } from 'api-client';
import { RobotTableData } from './robot-table';

// Define the stories
storiesOf('Components/RobotDataGridTable', module).add('Default', () => {
const robots: RobotTableData[] = [
{
fleet: 'Fleet A',
name: 'Robot 1',
estFinishTime: new Date('2023-05-01T09:00:00').getTime(),
battery: 0.8,
lastUpdateTime: new Date('2023-05-01T08:30:00').getTime(),
status: RobotStatus.Working,
},
{
fleet: 'Fleet B',
name: 'Robot 2',
estFinishTime: new Date('2023-05-01T10:30:00').getTime(),
battery: 0.6,
lastUpdateTime: new Date('2023-05-01T10:00:00').getTime(),
status: RobotStatus.Charging,
},
{
fleet: 'Fleet A',
name: 'Robot 3',
estFinishTime: new Date('2023-05-01T11:45:00').getTime(),
battery: 0.9,
lastUpdateTime: new Date('2023-05-01T11:30:00').getTime(),
status: RobotStatus.Idle,
},
];

return <RobotDataGridTable robots={robots} />;
});
149 changes: 149 additions & 0 deletions packages/react-components/lib/robots/robot-table-datagrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {
DataGrid,
GridColDef,
GridEventListener,
GridValueGetterParams,
MuiEvent,
GridRowParams,
GridCellParams,
} from '@mui/x-data-grid';
import { styled } from '@mui/material';
import * as React from 'react';
import { Status2 } from 'api-client';
import { RobotTableData } from './robot-table';

const classes = {
robotErrorCell: 'MuiDataGrid-cell-error-cell',
robotChargingCell: 'MuiDataGrid-cell-charging-cell',
robotWorkingCell: 'MuiDataGrid-cell-working-cell',
robotIdleCell: 'MuiDataGrid-cell-idle-cell',
robotOfflineCell: 'MuiDataGrid-cell-offline-cell',
robotShutdownCell: 'MuiDataGrid-cell-shutdown-cell',
robotDefaultCell: 'MuiDataGrid-cell-defautl-cell',
};

const StyledDataGrid = styled(DataGrid)(({ theme }) => ({
[`& .${classes.robotErrorCell}`]: {
backgroundColor: theme.palette.error.main,
color: theme.palette.getContrastText(theme.palette.success.light),
},
[`& .${classes.robotChargingCell}`]: {
backgroundColor: theme.palette.info.main,
color: theme.palette.getContrastText(theme.palette.grey[500]),
},
[`& .${classes.robotWorkingCell}`]: {
backgroundColor: theme.palette.success.main,
color: theme.palette.getContrastText(theme.palette.info.light),
},
[`& .${classes.robotDefaultCell}`]: {
backgroundColor: theme.palette.warning.main,
color: theme.palette.getContrastText(theme.palette.warning.main),
},
}));

export interface RobotDataGridTableProps {
onRobotClick?(ev: MuiEvent<React.MouseEvent<HTMLElement>>, robotName: RobotTableData): void;
robots: RobotTableData[];
}

export function RobotDataGridTable({ onRobotClick, robots }: RobotDataGridTableProps): JSX.Element {
const handleEvent: GridEventListener<'rowClick'> = (
params: GridRowParams,
event: MuiEvent<React.MouseEvent<HTMLElement>>,
) => {
if (onRobotClick) {
onRobotClick(event, params.row);
}
};

const columns: GridColDef[] = [
{
field: 'fleet',
headerName: 'Fleet',
width: 90,
valueGetter: (params: GridValueGetterParams) => params.row.fleet,
flex: 1,
filterable: true,
},
{
field: 'name',
headerName: 'Robot Name',
width: 150,
editable: false,
valueGetter: (params: GridValueGetterParams) => params.row.name,
flex: 1,
filterable: true,
},
{
field: 'estFinishTime',
headerName: 'Est. Task Finish Time',
width: 150,
editable: false,
valueGetter: (params: GridValueGetterParams) =>
params.row.estFinishTime ? new Date(params.row.estFinishTime).toLocaleString() : '-',
flex: 1,
filterable: true,
},
{
field: 'battery',
headerName: 'Battery',
width: 150,
editable: false,
valueGetter: (params: GridValueGetterParams) => (params.row.battery * 100).toFixed(2),
flex: 1,
filterable: true,
},
{
field: 'lastUpdateTime',
headerName: 'Last Updated',
width: 150,
editable: false,
valueGetter: (params: GridValueGetterParams) =>
params.row.lastUpdateTime ? new Date(params.row.lastUpdateTime).toLocaleString() : '-',
flex: 1,
filterable: true,
},
{
field: 'status',
headerName: 'Status',
editable: false,
flex: 1,
filterable: true,
},
];

return (
<div style={{ height: '100%', width: '100%' }}>
<StyledDataGrid
autoHeight={true}
getRowId={(r) => r.name}
rows={robots}
pageSize={5}
rowHeight={38}
columns={columns}
rowsPerPageOptions={[5]}
onRowClick={handleEvent}
getCellClassName={(params: GridCellParams<string>) => {
if (params.field === 'status') {
switch (params.value) {
case Status2.Error:
return classes.robotErrorCell;
case Status2.Charging:
return classes.robotChargingCell;
case Status2.Working:
return classes.robotWorkingCell;
case Status2.Idle:
case Status2.Offline:
case Status2.Shutdown:
case Status2.Uninitialized:
return classes.robotDefaultCell;
default:
return classes.robotDefaultCell;
}
}
return '';
}}
/>
</div>
);
}

0 comments on commit 161e7e3

Please sign in to comment.