Skip to content

Commit

Permalink
chore(dashboard): toggle between view and edit mode (#609)
Browse files Browse the repository at this point in the history
  • Loading branch information
corteggiano authored Feb 27, 2023
1 parent da05cfc commit 22a404a
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 24 deletions.
34 changes: 29 additions & 5 deletions packages/dashboard/src/components/actions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import React from 'react';
import { useDispatch } from 'react-redux';

import { Button } from '@cloudscape-design/components';

import { DashboardMessages } from '~/messages';
import { DashboardState, SaveableDashboard } from '~/store/state';
import { onToggleReadOnly } from '~/store/actions';

export type ActionsProps = {
onSave: (dashboard: SaveableDashboard) => void;
messageOverrides: DashboardMessages;
grid: DashboardState['grid'];
readOnly: boolean;
hasEditPermission: boolean;
dashboardConfiguration: DashboardState['dashboardConfiguration'];
assetsDescriptionMap: DashboardState['assetsDescriptionMap'];
onSave?: (dashboard: SaveableDashboard) => void;
};

const Actions: React.FC<ActionsProps> = ({
onSave,
grid,
dashboardConfiguration,
messageOverrides,
assetsDescriptionMap,
hasEditPermission,
readOnly,
onSave,
}) => {
const dispatch = useDispatch();

const handleOnSave = () => {
if (!onSave) return;
const { height, width, cellSize, stretchToFit } = grid;
onSave({
grid: { height, width, cellSize, stretchToFit },
Expand All @@ -29,12 +38,27 @@ const Actions: React.FC<ActionsProps> = ({
});
};

const handleOnReadOnly = () => {
dispatch(onToggleReadOnly());
};

if (!onSave && !hasEditPermission) return <></>;

return (
<div className='actions'>
<h1 className='iot-dashboard-toolbar-title'>{messageOverrides.toolbar.actions.title}</h1>
<Button variant='primary' onClick={handleOnSave}>
{messageOverrides.toolbar.actions.save}
</Button>
<div className='button-actions'>
{onSave && (
<Button variant='primary' onClick={handleOnSave} data-test-id='actions-save-dashboard-btn'>
{messageOverrides.toolbar.actions.save}
</Button>
)}
{hasEditPermission && (
<Button variant='primary' onClick={handleOnReadOnly} data-test-id='actions-toggle-read-only-btn'>
{readOnly ? 'Edit' : 'Preview'}
</Button>
)}
</div>
</div>
);
};
Expand Down
11 changes: 10 additions & 1 deletion packages/dashboard/src/components/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ export type DashboardProps = {
query?: SiteWiseQuery;
onSave?: (dashboard: SaveableDashboard) => void;
client?: IoTSiteWiseClient;
hasEditPermission?: boolean;
} & PickRequiredOptional<DashboardState, 'dashboardConfiguration', 'readOnly' | 'grid'>;

const Dashboard: React.FC<DashboardProps> = ({ messageOverrides, query, onSave, client, ...dashboardState }) => {
const Dashboard: React.FC<DashboardProps> = ({
messageOverrides,
query,
onSave,
hasEditPermission = true,
client,
...dashboardState
}) => {
return (
<ClientContext.Provider value={client}>
<Provider store={configureDashboardStore({ ...dashboardState }, client)}>
Expand All @@ -38,6 +46,7 @@ const Dashboard: React.FC<DashboardProps> = ({ messageOverrides, query, onSave,
<InternalDashboard
query={query}
onSave={onSave}
hasEditPermission={hasEditPermission}
messageOverrides={merge(messageOverrides, DefaultDashboardMessages)}
/>
</DndProvider>
Expand Down
13 changes: 11 additions & 2 deletions packages/dashboard/src/components/internalDashboard/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@
border-bottom: var(--border-width-toolbar) solid var(--colors-light-grey);
z-index: var(--stack-order-grid-inputs);
background-color: var(--colors-white);
box-sizing: border-box;
height: var(--toolbar-overlay-height);
}

.iot-dashboard-toolbar-overlay {
position: fixed;
left: 0;
right: 0;
height: var(--toolbar-overlay-height);
box-sizing: border-box;
}

Expand All @@ -54,10 +55,18 @@
padding-left: initial;
}

.button-actions {
display: flex;
align-items: flex-start;
flex-direction: row;
justify-content: center;
gap: var(--button-action-gap);
}

.iot-dashboard-toolbar-title {
font-size: var(--font-size-component-palette-title);
margin: 0;
margin-bottom: 0.67rem;
margin-bottom: 0.5rem;
}

.dummy-content {
Expand Down
95 changes: 91 additions & 4 deletions packages/dashboard/src/components/internalDashboard/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('InternalDashboard', () => {
enableKeyboardEvents: true,
}}
>
<InternalDashboard query={undefined} messageOverrides={DefaultDashboardMessages} />
<InternalDashboard hasEditPermission={true} query={undefined} messageOverrides={DefaultDashboardMessages} />
</DndProvider>
</Provider>,
container
Expand Down Expand Up @@ -75,7 +75,12 @@ describe('InternalDashboard', () => {
enableKeyboardEvents: true,
}}
>
<InternalDashboard query={undefined} messageOverrides={DefaultDashboardMessages} onSave={onSave} />
<InternalDashboard
hasEditPermission={true}
query={undefined}
messageOverrides={DefaultDashboardMessages}
onSave={onSave}
/>
</DndProvider>
</Provider>,
container
Expand All @@ -86,7 +91,7 @@ describe('InternalDashboard', () => {

act(() => {
if (!actionsContainer) throw new Error('No actions on dashboard');
wrapper(actionsContainer).findButton()?.click();
wrapper(actionsContainer).findButton('[data-test-id="actions-save-dashboard-btn"]')?.click();
});

expect(onSave).toBeCalledWith(args);
Expand Down Expand Up @@ -114,7 +119,7 @@ describe('InternalDashboard', () => {
enableKeyboardEvents: true,
}}
>
<InternalDashboard query={undefined} messageOverrides={DefaultDashboardMessages} />
<InternalDashboard hasEditPermission={true} query={undefined} messageOverrides={DefaultDashboardMessages} />
</DndProvider>
</Provider>,
container
Expand All @@ -124,4 +129,86 @@ describe('InternalDashboard', () => {
expect(container.querySelector('.viewport-selection')).toBeTruthy();
expect(container.querySelector('.iot-dashboard-panes-area')).toBeFalsy();
});

it('can toggle to readonly mode', function () {
const container = document.createElement('div');
document.body.appendChild(container);

const args = {
readOnly: false,
dashboardConfiguration: {
widgets: [],
viewport: { duration: '5m' },
},
};

act(() => {
ReactDOM.render(
<Provider store={configureDashboardStore(args)}>
<DndProvider
backend={TouchBackend}
options={{
enableMouseEvents: true,
enableKeyboardEvents: true,
}}
>
<InternalDashboard hasEditPermission={true} query={undefined} messageOverrides={DefaultDashboardMessages} />
</DndProvider>
</Provider>,
container
);
});

const actionsContainer = container.querySelector('.button-actions');
expect(actionsContainer).toBeTruthy();

act(() => {
if (!actionsContainer) throw new Error('No actions on dashboard');
const foundButton = wrapper(actionsContainer).findButton('[data-test-id="actions-toggle-read-only-btn"]');
foundButton?.click();
});

const dashboard = container.querySelector('.iot-dashboard-panes-area');
expect(dashboard).toBeFalsy();
});

it('cannot toggle to edit mode if no edit permissions', function () {
const container = document.createElement('div');
document.body.appendChild(container);

const args = {
readOnly: true,
dashboardConfiguration: {
widgets: [],
viewport: { duration: '5m' },
},
};

act(() => {
ReactDOM.render(
<Provider store={configureDashboardStore(args)}>
<DndProvider
backend={TouchBackend}
options={{
enableMouseEvents: true,
enableKeyboardEvents: true,
}}
>
<InternalDashboard
hasEditPermission={false}
query={undefined}
messageOverrides={DefaultDashboardMessages}
/>
</DndProvider>
</Provider>,
container
);
});

const actionsContainer = container.querySelector('.button-actions');
expect(actionsContainer).toBeFalsy();

const dashboard = container.querySelector('.iot-dashboard-panes-area');
expect(dashboard).toBeFalsy();
});
});
35 changes: 25 additions & 10 deletions packages/dashboard/src/components/internalDashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,17 @@ import { useKeyboardShortcuts } from './keyboardShortcuts';

type InternalDashboardProps = {
messageOverrides: DashboardMessages;
hasEditPermission: boolean;
query?: SiteWiseQuery;
onSave?: (dashboard: SaveableDashboard) => void;
};

const InternalDashboard: React.FC<InternalDashboardProps> = ({ messageOverrides, query, onSave }) => {
const InternalDashboard: React.FC<InternalDashboardProps> = ({
messageOverrides,
hasEditPermission,
query,
onSave,
}) => {
/**
* Store variables
*/
Expand Down Expand Up @@ -179,6 +185,15 @@ const InternalDashboard: React.FC<InternalDashboardProps> = ({ messageOverrides,
<div className='iot-dashboard'>
<div className='iot-dashboard-toolbar iot-dashboard-toolbar-overlay'>
<ViewportSelection viewport={viewport} messageOverrides={messageOverrides} />
<Actions
hasEditPermission={hasEditPermission}
messageOverrides={messageOverrides}
readOnly={readOnly}
onSave={onSave}
dashboardConfiguration={dashboardConfiguration}
grid={grid}
assetsDescriptionMap={assetsDescriptionMap}
/>
</div>
<div className='iot-dashboard-grid iot-dashboard-grid-with-overlay'>
<Grid {...gridProps}>
Expand All @@ -196,15 +211,15 @@ const InternalDashboard: React.FC<InternalDashboardProps> = ({ messageOverrides,
<div className='iot-dashboard-toolbar'>
<ComponentPalette messageOverrides={messageOverrides} />
<ViewportSelection viewport={viewport} messageOverrides={messageOverrides} />
{onSave && (
<Actions
messageOverrides={messageOverrides}
onSave={onSave}
dashboardConfiguration={dashboardConfiguration}
grid={grid}
assetsDescriptionMap={assetsDescriptionMap}
/>
)}
<Actions
hasEditPermission={hasEditPermission}
readOnly={readOnly}
messageOverrides={messageOverrides}
onSave={onSave}
dashboardConfiguration={dashboardConfiguration}
grid={grid}
assetsDescriptionMap={assetsDescriptionMap}
/>
</div>
<div className='iot-dashboard-panes-area'>
<ResizablePanes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const renderDashboardAndPressKey = ({ key, meta }: { key: string; meta: boolean
enableKeyboardEvents: true,
}}
>
<InternalDashboard query={undefined} messageOverrides={DefaultDashboardMessages} />
<InternalDashboard hasEditPermission={true} query={undefined} messageOverrides={DefaultDashboardMessages} />
</DndProvider>
</Provider>,
container
Expand Down
3 changes: 3 additions & 0 deletions packages/dashboard/src/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CreateWidgetsAction } from './createWidget';
import { SelectWidgetsAction } from './selectWidgets';
import { ToggleReadOnlyAction } from './toggleReadOnly';
import { MoveWidgetsAction } from './moveWidgets';
import { ResizeWidgetsAction } from './resizeWidgets';
import {
Expand Down Expand Up @@ -30,6 +31,7 @@ export * from './resizeWidgets';
export * from './updateWidget';
export * from './changeDashboardGrid';
export * from './updateViewport';
export * from './toggleReadOnly';

export type DashboardAction =
| CreateWidgetsAction
Expand All @@ -38,6 +40,7 @@ export type DashboardAction =
| CopyWidgetsAction
| PasteWidgetsAction
| MoveWidgetsAction
| ToggleReadOnlyAction
| BringWidgetsToFrontAction
| SendWidgetsToBackAction
| ResizeWidgetsAction
Expand Down
29 changes: 29 additions & 0 deletions packages/dashboard/src/store/actions/toggleReadOnly/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { toggleReadOnly } from '.';
import { DashboardState, initialState } from '../../state';

import { MOCK_KPI_WIDGET, MOCK_LINE_CHART_WIDGET } from '../../../../testing/mocks';
import { Widget } from '~/types';

const setupDashboardState = (widgets: Widget[] = [], pasteCounter = 0): DashboardState => ({
...initialState,
dashboardConfiguration: {
...initialState.dashboardConfiguration,
widgets,
},
pasteCounter,
readOnly: false,
});

it('can toggle the state if no widgets are present', () => {
expect(toggleReadOnly(setupDashboardState([])).readOnly).toEqual(true);
});

it('can toggle the state if widgets are present', () => {
expect(toggleReadOnly(setupDashboardState([MOCK_KPI_WIDGET, MOCK_LINE_CHART_WIDGET])).readOnly).toEqual(true);
});

it('can toggle the state back and forth', () => {
expect(
toggleReadOnly(toggleReadOnly(setupDashboardState([MOCK_KPI_WIDGET, MOCK_LINE_CHART_WIDGET]))).readOnly
).toEqual(false);
});
Loading

0 comments on commit 22a404a

Please sign in to comment.