Skip to content

Commit

Permalink
feat: dashboard lock feature (#3880)
Browse files Browse the repository at this point in the history
* feat: dashboard lock feature

* feat: update API method and minor ui updates

* feat: update API and author logic

* feat: update permissions for author role

* feat: use strings and remove console logs
  • Loading branch information
YounixM authored Nov 3, 2023
1 parent 8371670 commit 0906886
Show file tree
Hide file tree
Showing 22 changed files with 266 additions and 85 deletions.
4 changes: 3 additions & 1 deletion frontend/public/locales/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@
"variable_updated_successfully": "Variable updated successfully",
"error_while_updating_variable": "Error while updating variable",
"dashboard_has_been_updated": "Dashboard has been updated",
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?"
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.",
"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard."
}
11 changes: 11 additions & 0 deletions frontend/src/api/dashboard/lockDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';

interface LockDashboardProps {
uuid: string;
}

const lockDashboard = (props: LockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/lock`);

export default lockDashboard;
11 changes: 11 additions & 0 deletions frontend/src/api/dashboard/unlockDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';

interface UnlockDashboardProps {
uuid: string;
}

const unlockDashboard = (props: UnlockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/unlock`);

export default unlockDashboard;
84 changes: 56 additions & 28 deletions frontend/src/container/GridCardLayout/GridCardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';

import { headerMenuList } from './config';
import { EditMenuAction, ViewMenuAction } from './config';
import GridCard from './GridCard';
import {
Button,
Expand All @@ -32,10 +34,11 @@ function GraphLayout({
layouts,
setLayouts,
setSelectedDashboard,
isDashboardLocked,
} = useDashboard();
const { t } = useTranslation(['dashboard']);

const { featureResponse, role } = useSelector<AppState, AppReducer>(
const { featureResponse, role, user } = useSelector<AppState, AppReducer>(
(state) => state.app,
);

Expand All @@ -45,9 +48,20 @@ function GraphLayout({

const { notifications } = useNotifications();

let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];

if (isDashboardLocked) {
permissions = ['edit_locked_dashboard', 'add_panel_locked_dashboard'];
}

const userRole: ROLES | null =
selectedDashboard?.created_by === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: role;

const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
['save_layout', 'add_panel'],
role,
permissions,
userRole,
);

const onSaveHandler = (): void => {
Expand Down Expand Up @@ -83,35 +97,41 @@ function GraphLayout({
});
};

const widgetActions = !isDashboardLocked
? [...ViewMenuAction, ...EditMenuAction]
: [...ViewMenuAction];

return (
<>
<ButtonContainer>
{saveLayoutPermission && (
<Button
loading={updateDashboardMutation.isLoading}
onClick={onSaveHandler}
icon={<SaveFilled />}
disabled={updateDashboardMutation.isLoading}
>
{t('dashboard:save_layout')}
</Button>
)}

{addPanelPermission && (
<Button onClick={onAddPanelHandler} icon={<PlusOutlined />}>
{t('dashboard:add_panel')}
</Button>
)}
</ButtonContainer>
{!isDashboardLocked && (
<ButtonContainer>
{saveLayoutPermission && (
<Button
loading={updateDashboardMutation.isLoading}
onClick={onSaveHandler}
icon={<SaveFilled />}
disabled={updateDashboardMutation.isLoading}
>
{t('dashboard:save_layout')}
</Button>
)}

{addPanelPermission && (
<Button onClick={onAddPanelHandler} icon={<PlusOutlined />}>
{t('dashboard:add_panel')}
</Button>
)}
</ButtonContainer>
)}

<ReactGridLayout
cols={12}
rowHeight={100}
autoSize
width={100}
isDraggable={addPanelPermission}
isDroppable={addPanelPermission}
isResizable={addPanelPermission}
isDraggable={!isDashboardLocked && addPanelPermission}
isDroppable={!isDashboardLocked && addPanelPermission}
isResizable={!isDashboardLocked && addPanelPermission}
allowOverlap={false}
onLayoutChange={setLayouts}
draggableHandle=".drag-handle"
Expand All @@ -122,12 +142,20 @@ function GraphLayout({
const currentWidget = (widgets || [])?.find((e) => e.id === id);

return (
<CardContainer isDarkMode={isDarkMode} key={id} data-grid={layout}>
<Card $panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}>
<CardContainer
className={isDashboardLocked ? '' : 'enable-resize'}
isDarkMode={isDarkMode}
key={id}
data-grid={layout}
>
<Card
className="grid-item"
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
>
<GridCard
widget={currentWidget || ({ id, query: {} } as Widgets)}
name={currentWidget?.id || ''}
headerMenuList={headerMenuList}
headerMenuList={widgetActions}
/>
</Card>
</CardContainer>
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/container/GridCardLayout/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';

export const headerMenuList = [
MenuItemKeys.View,
export const ViewMenuAction = [MenuItemKeys.View];

export const EditMenuAction = [
MenuItemKeys.Clone,
MenuItemKeys.Delete,
MenuItemKeys.Edit,
];

export const headerMenuList = [...ViewMenuAction];

export const EMPTY_WIDGET_LAYOUT = {
i: PANEL_TYPES.EMPTY_WIDGET,
w: 6,
Expand Down
44 changes: 23 additions & 21 deletions frontend/src/container/GridCardLayout/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,31 @@ interface Props {
export const CardContainer = styled.div<Props>`
overflow: auto;
:hover {
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: se-resize;
&.enable-resize {
:hover {
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: se-resize;
${({ isDarkMode }): StyledCSS => {
const uri = `data:image/svg+xml,%3Csvg viewBox='0 0 6 6' style='background-color:%23ffffff00' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' x='0px' y='0px' width='6px' height='6px'%0A%3E%3Cg opacity='0.302'%3E%3Cpath d='M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z' fill='${
isDarkMode ? 'white' : 'grey'
}'/%3E%3C/g%3E%3C/svg%3E`;
${({ isDarkMode }): StyledCSS => {
const uri = `data:image/svg+xml,%3Csvg viewBox='0 0 6 6' style='background-color:%23ffffff00' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' x='0px' y='0px' width='6px' height='6px'%0A%3E%3Cg opacity='0.302'%3E%3Cpath d='M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z' fill='${
isDarkMode ? 'white' : 'grey'
}'/%3E%3C/g%3E%3C/svg%3E`;
return css`
background-image: ${(): string => `url("${uri}")`};
`;
}}
return css`
background-image: ${(): string => `url("${uri}")`};
`;
}}
}
}
}
`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { Modal } from 'antd';
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { Modal, Tooltip } from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';

import { Data } from '../index';
import { TableLinkText } from './styles';

function DeleteButton({ id }: Data): JSX.Element {
function DeleteButton({ id, createdBy, isLocked }: Data): JSX.Element {
const [modal, contextHolder] = Modal.useModal();
const { role, user } = useSelector<AppState, AppReducer>((state) => state.app);
const isAuthor = user?.email === createdBy;

const queryClient = useQueryClient();

const { t } = useTranslation(['dashboard']);

const deleteDashboardMutation = useDeleteDashboard(id);

const openConfirmationDialog = useCallback((): void => {
Expand All @@ -32,11 +41,33 @@ function DeleteButton({ id }: Data): JSX.Element {
});
}, [modal, deleteDashboardMutation, queryClient]);

const getDeleteTooltipContent = (): string => {
if (isLocked) {
if (role === USER_ROLES.ADMIN || isAuthor) {
return t('dashboard:locked_dashboard_delete_tooltip_admin_author');
}

return t('dashboard:locked_dashboard_delete_tooltip_editor');
}

return '';
};

return (
<>
<TableLinkText type="danger" onClick={openConfirmationDialog}>
Delete
</TableLinkText>
<Tooltip placement="left" title={getDeleteTooltipContent()}>
<TableLinkText
type="danger"
onClick={(): void => {
if (!isLocked) {
openConfirmationDialog();
}
}}
disabled={isLocked}
>
<DeleteOutlined /> Delete
</TableLinkText>
</Tooltip>

{contextHolder}
</>
Expand All @@ -55,6 +86,7 @@ function Wrapper(props: Data): JSX.Element {
tags,
createdBy,
lastUpdatedBy,
isLocked,
} = props;

return (
Expand All @@ -69,6 +101,7 @@ function Wrapper(props: Data): JSX.Element {
tags,
createdBy,
lastUpdatedBy,
isLocked,
}}
/>
);
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/container/ListOfDashboard/TableComponents/Name.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LockFilled } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { generatePath } from 'react-router-dom';
Expand All @@ -6,17 +7,21 @@ import { Data } from '..';
import { TableLinkText } from './styles';

function Name(name: Data['name'], data: Data): JSX.Element {
const onClickHandler = (): void => {
const { id: DashboardId } = data;
const { id: DashboardId, isLocked } = data;

const onClickHandler = (): void => {
history.push(
generatePath(ROUTES.DASHBOARD, {
dashboardId: DashboardId,
}),
);
};

return <TableLinkText onClick={onClickHandler}>{name}</TableLinkText>;
return (
<TableLinkText onClick={onClickHandler}>
{isLocked && <LockFilled />} {name}
</TableLinkText>
);
}

export default Name;
4 changes: 3 additions & 1 deletion frontend/src/container/ListOfDashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function ListOfAllDashboard(): JSX.Element {
dataIndex: 'description',
},
{
title: 'Tags (can be multiple)',
title: 'Tags',
dataIndex: 'tags',
width: 50,
render: (value): JSX.Element => <LabelColumn labels={value} />,
Expand Down Expand Up @@ -159,6 +159,7 @@ function ListOfAllDashboard(): JSX.Element {
tags: e.data.tags || [],
key: e.uuid,
createdBy: e.created_by,
isLocked: !!e.isLocked || false,
lastUpdatedBy: e.updated_by,
refetchDashboardList,
})) || [];
Expand Down Expand Up @@ -342,6 +343,7 @@ export interface Data {
createdAt: string;
lastUpdatedTime: string;
lastUpdatedBy: string;
isLocked: boolean;
id: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import styled from 'styled-components';

export const Container = styled.div`
display: flex;
gap: 0.6rem;
justify-content: right;
gap: 8px;
`;

export const Card = styled(CardComponent)`
min-height: 10vh;
min-width: 120px;
overflow-y: auto;
cursor: pointer;
Expand Down
Loading

0 comments on commit 0906886

Please sign in to comment.