mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-20 12:49:11 +08:00
feat: dashboard lock feature (#3880)
* 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
This commit is contained in:
parent
8371670512
commit
0906886e9a
@ -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
frontend/src/api/dashboard/lockDashboard.ts
Normal file
11
frontend/src/api/dashboard/lockDashboard.ts
Normal file
@ -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
frontend/src/api/dashboard/unlockDashboard.ts
Normal file
11
frontend/src/api/dashboard/unlockDashboard.ts
Normal file
@ -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;
|
@ -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,
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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 => {
|
||||
@ -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>
|
||||
)}
|
||||
{!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>
|
||||
{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"
|
||||
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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}")`};
|
||||
`;
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -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 => {
|
||||
@ -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}
|
||||
</>
|
||||
@ -55,6 +86,7 @@ function Wrapper(props: Data): JSX.Element {
|
||||
tags,
|
||||
createdBy,
|
||||
lastUpdatedBy,
|
||||
isLocked,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@ -69,6 +101,7 @@ function Wrapper(props: Data): JSX.Element {
|
||||
tags,
|
||||
createdBy,
|
||||
lastUpdatedBy,
|
||||
isLocked,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -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';
|
||||
@ -6,9 +7,9 @@ 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,
|
||||
@ -16,7 +17,11 @@ function Name(name: Data['name'], data: Data): JSX.Element {
|
||||
);
|
||||
};
|
||||
|
||||
return <TableLinkText onClick={onClickHandler}>{name}</TableLinkText>;
|
||||
return (
|
||||
<TableLinkText onClick={onClickHandler}>
|
||||
{isLocked && <LockFilled />} {name}
|
||||
</TableLinkText>
|
||||
);
|
||||
}
|
||||
|
||||
export default Name;
|
||||
|
@ -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} />,
|
||||
@ -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,
|
||||
})) || [];
|
||||
@ -342,6 +343,7 @@ export interface Data {
|
||||
createdAt: string;
|
||||
lastUpdatedTime: string;
|
||||
lastUpdatedBy: string;
|
||||
isLocked: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
import Input from 'components/Input';
|
||||
import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react';
|
||||
|
||||
function NameOfTheDashboard({
|
||||
setName,
|
||||
name,
|
||||
}: NameOfTheDashboardProps): JSX.Element {
|
||||
function DashboardName({ setName, name }: DashboardNameProps): JSX.Element {
|
||||
const onChangeHandler = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value);
|
||||
@ -22,9 +19,9 @@ function NameOfTheDashboard({
|
||||
);
|
||||
}
|
||||
|
||||
interface NameOfTheDashboardProps {
|
||||
interface DashboardNameProps {
|
||||
name: string;
|
||||
setName: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export default NameOfTheDashboard;
|
||||
export default DashboardName;
|
@ -18,7 +18,7 @@ function SettingsDrawer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="dashed" onClick={showDrawer}>
|
||||
<Button type="dashed" onClick={showDrawer} style={{ width: '100%' }}>
|
||||
<SettingOutlined /> Configure
|
||||
</Button>
|
||||
<DrawerContainer
|
@ -1,5 +1,5 @@
|
||||
import { ShareAltOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd';
|
||||
import { LockFilled, ShareAltOutlined, UnlockFilled } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Row, Space, Tag, Tooltip, Typography } from 'antd';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useState } from 'react';
|
||||
@ -7,13 +7,18 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||
import SettingsDrawer from './SettingsDrawer';
|
||||
import ShareModal from './ShareModal';
|
||||
|
||||
function DescriptionOfDashboard(): JSX.Element {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
function DashboardDescription(): JSX.Element {
|
||||
const {
|
||||
selectedDashboard,
|
||||
isDashboardLocked,
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
|
||||
const selectedData = selectedDashboard?.data;
|
||||
const { title, tags, description } = selectedData || {};
|
||||
@ -21,18 +26,33 @@ function DescriptionOfDashboard(): JSX.Element {
|
||||
const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation('common');
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const { user, role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [editDashboard] = useComponentPermission(['edit_dashboard'], role);
|
||||
|
||||
let isAuthor = false;
|
||||
|
||||
if (selectedDashboard && user && user.email) {
|
||||
isAuthor = selectedDashboard?.created_by === user?.email;
|
||||
}
|
||||
|
||||
const onToggleHandler = (): void => {
|
||||
isIsJSONModalVisible((state) => !state);
|
||||
};
|
||||
|
||||
const handleLockDashboardToggle = (): void => {
|
||||
handleDashboardLockToggle(!isDashboardLocked);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Row>
|
||||
<Col flex={1}>
|
||||
<Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
|
||||
{isDashboardLocked && (
|
||||
<Tooltip title="Dashboard Locked" placement="top">
|
||||
<LockFilled />
|
||||
</Tooltip>
|
||||
)}
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Typography>{description}</Typography>
|
||||
@ -55,7 +75,7 @@ function DescriptionOfDashboard(): JSX.Element {
|
||||
)}
|
||||
|
||||
<Space direction="vertical">
|
||||
{editDashboard && <SettingsDrawer />}
|
||||
{!isDashboardLocked && editDashboard && <SettingsDrawer />}
|
||||
<Button
|
||||
style={{ width: '100%' }}
|
||||
type="dashed"
|
||||
@ -64,6 +84,21 @@ function DescriptionOfDashboard(): JSX.Element {
|
||||
>
|
||||
{t('share')}
|
||||
</Button>
|
||||
{(isAuthor || role === USER_ROLES.ADMIN) && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
>
|
||||
<Button
|
||||
style={{ width: '100%' }}
|
||||
type="dashed"
|
||||
onClick={handleLockDashboardToggle}
|
||||
icon={isDashboardLocked ? <LockFilled /> : <UnlockFilled />}
|
||||
>
|
||||
{isDashboardLocked ? 'Unlock' : 'Lock'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -71,4 +106,4 @@ function DescriptionOfDashboard(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default DescriptionOfDashboard;
|
||||
export default DashboardDescription;
|
@ -1,4 +1,4 @@
|
||||
import Description from './DescriptionOfDashboard';
|
||||
import Description from './DashboardDescription';
|
||||
import GridGraphs from './GridGraphs';
|
||||
|
||||
function NewDashboard(): JSX.Element {
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Modal } from 'antd';
|
||||
import get from 'api/dashboard/get';
|
||||
import lockDashboardApi from 'api/dashboard/lockDashboard';
|
||||
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
import {
|
||||
@ -17,7 +20,7 @@ import {
|
||||
} from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { useMutation, useQuery, UseQueryResult } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
@ -32,7 +35,9 @@ import { IDashboardContext } from './types';
|
||||
|
||||
const DashboardContext = createContext<IDashboardContext>({
|
||||
isDashboardSliderOpen: false,
|
||||
isDashboardLocked: false,
|
||||
handleToggleDashboardSlider: () => {},
|
||||
handleDashboardLockToggle: () => {},
|
||||
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>,
|
||||
selectedDashboard: {} as Dashboard,
|
||||
dashboardId: '',
|
||||
@ -50,6 +55,9 @@ export function DashboardProvider({
|
||||
children,
|
||||
}: PropsWithChildren): JSX.Element {
|
||||
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
|
||||
|
||||
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
||||
|
||||
const isDashboardPage = useRouteMatch<Props>({
|
||||
path: ROUTES.DASHBOARD,
|
||||
exact: true,
|
||||
@ -99,6 +107,8 @@ export function DashboardProvider({
|
||||
onSuccess: (data) => {
|
||||
const updatedDate = dayjs(data.updated_at);
|
||||
|
||||
setIsDashboardLocked(data?.isLocked || false);
|
||||
|
||||
// on first render
|
||||
if (updatedTimeRef.current === null) {
|
||||
setSelectedDashboard(data);
|
||||
@ -179,10 +189,39 @@ export function DashboardProvider({
|
||||
setIsDashboardSlider(value);
|
||||
};
|
||||
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { mutate: lockDashboard } = useMutation(lockDashboardApi, {
|
||||
onSuccess: () => {
|
||||
setIsDashboardSlider(false);
|
||||
setIsDashboardLocked(true);
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const { mutate: unlockDashboard } = useMutation(unlockDashboardApi, {
|
||||
onSuccess: () => {
|
||||
setIsDashboardLocked(false);
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
|
||||
if (selectedDashboard) {
|
||||
if (value) {
|
||||
lockDashboard(selectedDashboard);
|
||||
} else {
|
||||
unlockDashboard(selectedDashboard);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const value: IDashboardContext = useMemo(
|
||||
() => ({
|
||||
isDashboardSliderOpen,
|
||||
isDashboardLocked,
|
||||
handleToggleDashboardSlider,
|
||||
handleDashboardLockToggle,
|
||||
dashboardResponse,
|
||||
selectedDashboard,
|
||||
dashboardId,
|
||||
@ -191,8 +230,10 @@ export function DashboardProvider({
|
||||
setSelectedDashboard,
|
||||
updatedTimeRef,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
isDashboardSliderOpen,
|
||||
isDashboardLocked,
|
||||
dashboardResponse,
|
||||
selectedDashboard,
|
||||
dashboardId,
|
||||
|
@ -5,7 +5,9 @@ import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface IDashboardContext {
|
||||
isDashboardSliderOpen: boolean;
|
||||
isDashboardLocked: boolean;
|
||||
handleToggleDashboardSlider: (value: boolean) => void;
|
||||
handleDashboardLockToggle: (value: boolean) => void;
|
||||
dashboardResponse: UseQueryResult<Dashboard, unknown>;
|
||||
selectedDashboard: Dashboard | undefined;
|
||||
dashboardId: string;
|
||||
|
@ -45,6 +45,7 @@ export interface Dashboard {
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
data: DashboardData;
|
||||
isLocked?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
|
@ -1,11 +1,13 @@
|
||||
export type ADMIN = 'ADMIN';
|
||||
export type VIEWER = 'VIEWER';
|
||||
export type EDITOR = 'EDITOR';
|
||||
export type AUTHOR = 'AUTHOR';
|
||||
|
||||
export type ROLES = ADMIN | VIEWER | EDITOR;
|
||||
export type ROLES = ADMIN | VIEWER | EDITOR | AUTHOR;
|
||||
|
||||
export const USER_ROLES = {
|
||||
ADMIN: 'ADMIN',
|
||||
VIEWER: 'VIEWER',
|
||||
EDITOR: 'EDITOR',
|
||||
AUTHOR: 'AUTHOR',
|
||||
};
|
||||
|
@ -18,7 +18,9 @@ export type ComponentTypes =
|
||||
| 'new_alert_action'
|
||||
| 'edit_widget'
|
||||
| 'add_panel'
|
||||
| 'page_pipelines';
|
||||
| 'page_pipelines'
|
||||
| 'edit_locked_dashboard'
|
||||
| 'add_panel_locked_dashboard';
|
||||
|
||||
export const componentPermission: Record<ComponentTypes, ROLES[]> = {
|
||||
current_org_settings: ['ADMIN'],
|
||||
@ -30,14 +32,16 @@ export const componentPermission: Record<ComponentTypes, ROLES[]> = {
|
||||
add_new_channel: ['ADMIN'],
|
||||
set_retention_period: ['ADMIN'],
|
||||
action: ['ADMIN', 'EDITOR'],
|
||||
save_layout: ['ADMIN', 'EDITOR'],
|
||||
edit_dashboard: ['ADMIN', 'EDITOR'],
|
||||
delete_widget: ['ADMIN', 'EDITOR'],
|
||||
save_layout: ['ADMIN', 'EDITOR', 'AUTHOR'],
|
||||
edit_dashboard: ['ADMIN', 'EDITOR', 'AUTHOR'],
|
||||
delete_widget: ['ADMIN', 'EDITOR', 'AUTHOR'],
|
||||
new_dashboard: ['ADMIN', 'EDITOR'],
|
||||
new_alert_action: ['ADMIN'],
|
||||
edit_widget: ['ADMIN', 'EDITOR'],
|
||||
add_panel: ['ADMIN', 'EDITOR'],
|
||||
add_panel: ['ADMIN', 'EDITOR', 'AUTHOR'],
|
||||
page_pipelines: ['ADMIN', 'EDITOR'],
|
||||
edit_locked_dashboard: ['ADMIN', 'AUTHOR'],
|
||||
add_panel_locked_dashboard: ['ADMIN', 'AUTHOR'],
|
||||
};
|
||||
|
||||
export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user