feat: [SIG-546]: user with viewer roles can only view saved views (#4663)

* feat: [SIG-543]: Users with VIEWER access can create/edit/delete views for logs and traces

* feat: [SIG-543]: remove extra code

* feat: [SIG-543]: role changes in the save views toolbar

* feat: [SIG-543]: role changes in the save views toolbar

* feat: remove the save feature / dashboard / alert feature for viewer roles

* feat: remove the save feature / dashboard / alert feature for viewer roles

* fix: address review comments
This commit is contained in:
Vikrant Gupta 2024-03-11 14:49:10 +05:30 committed by GitHub
parent 49aba4fb1c
commit 6b87118fc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 331 additions and 283 deletions

View File

@ -1,3 +1,6 @@
.hide-update {
left: calc(50% - 41px) !important;
}
.explorer-update { .explorer-update {
position: fixed; position: fixed;
bottom: 16px; bottom: 16px;
@ -23,6 +26,10 @@
cursor: pointer; cursor: pointer;
} }
.hidden {
display: none;
}
.ant-divider { .ant-divider {
margin: 0; margin: 0;
height: 28px; height: 28px;
@ -55,6 +62,10 @@
.view-options, .view-options,
.actions { .actions {
.hidden {
display: none;
}
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -102,6 +113,9 @@
} }
} }
} }
.hidden {
display: none;
}
} }
.app-content { .app-content {

View File

@ -13,6 +13,7 @@ import {
Typography, Typography,
} from 'antd'; } from 'antd';
import axios from 'axios'; import axios from 'axios';
import cx from 'classnames';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils'; import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
@ -31,10 +32,14 @@ import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery'; import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { Check, ConciergeBell, Disc3, Plus, X, XCircle } from 'lucide-react'; import { Check, ConciergeBell, Disc3, Plus, X, XCircle } from 'lucide-react';
import { CSSProperties, useCallback, useMemo, useRef, useState } from 'react'; import { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { import {
DATASOURCE_VS_ROUTES, DATASOURCE_VS_ROUTES,
@ -43,6 +48,9 @@ import {
saveNewViewHandler, saveNewViewHandler,
} from './utils'; } from './utils';
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
// eslint-disable-next-line sonarjs/cognitive-complexity
function ExplorerOptions({ function ExplorerOptions({
disabled, disabled,
isLoading, isLoading,
@ -71,6 +79,8 @@ function ExplorerOptions({
setIsSaveModalOpen(false); setIsSaveModalOpen(false);
}; };
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const onCreateAlertsHandler = useCallback(() => { const onCreateAlertsHandler = useCallback(() => {
history.push( history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent( `${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
@ -247,10 +257,17 @@ function ExplorerOptions({
[isDarkMode], [isDarkMode],
); );
const isEditDeleteSupported = allowedRoles.includes(role as string);
return ( return (
<> <>
{isQueryUpdated && ( {isQueryUpdated && (
<div className="explorer-update"> <div
className={cx(
isEditDeleteSupported ? '' : 'hide-update',
'explorer-update',
)}
>
<Tooltip title="Clear this view" placement="top"> <Tooltip title="Clear this view" placement="top">
<Button <Button
className="action-icon" className="action-icon"
@ -258,10 +275,13 @@ function ExplorerOptions({
icon={<X size={14} />} icon={<X size={14} />}
/> />
</Tooltip> </Tooltip>
<Divider type="vertical" /> <Divider
type="vertical"
className={isEditDeleteSupported ? '' : 'hidden'}
/>
<Tooltip title="Update this view" placement="top"> <Tooltip title="Update this view" placement="top">
<Button <Button
className="action-icon" className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
disabled={isViewUpdating} disabled={isViewUpdating}
onClick={onUpdateQueryHandler} onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />} icon={<Disc3 size={14} />}
@ -323,15 +343,16 @@ function ExplorerOptions({
<Button <Button
shape="round" shape="round"
onClick={handleSaveViewModalToggle} onClick={handleSaveViewModalToggle}
className={isEditDeleteSupported ? '' : 'hidden'}
disabled={viewsIsLoading || isRefetching} disabled={viewsIsLoading || isRefetching}
> >
<Disc3 size={16} /> Save this view <Disc3 size={16} /> Save this view
</Button> </Button>
</div> </div>
<hr /> <hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className="actions"> <div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
<Tooltip title="Create Alerts"> <Tooltip title="Create Alerts">
<Button <Button
disabled={disabled} disabled={disabled}

View File

@ -309,49 +309,52 @@ function DashboardsList(): JSX.Element {
/> />
</Col> </Col>
<Col {createNewDashboard && (
span={6} <Col
style={{ span={6}
display: 'flex', style={{
justifyContent: 'flex-end', display: 'flex',
}} justifyContent: 'flex-end',
> }}
<ButtonContainer>
<TextToolTip
{...{
text: `More details on how to create dashboards`,
url: 'https://signoz.io/docs/userguide/dashboards',
}}
/>
</ButtonContainer>
<Dropdown
menu={{ items: getMenuItems }}
disabled={isDashboardListLoading}
placement="bottomRight"
> >
<NewDashboardButton <ButtonContainer>
icon={<PlusOutlined />} <TextToolTip
type="primary" {...{
data-testid="create-new-dashboard" text: `More details on how to create dashboards`,
loading={newDashboardState.loading} url: 'https://signoz.io/docs/userguide/dashboards',
danger={newDashboardState.error} }}
/>
</ButtonContainer>
<Dropdown
menu={{ items: getMenuItems }}
disabled={isDashboardListLoading}
placement="bottomRight"
> >
{getText()} <NewDashboardButton
</NewDashboardButton> icon={<PlusOutlined />}
</Dropdown> type="primary"
</Col> data-testid="create-new-dashboard"
loading={newDashboardState.loading}
danger={newDashboardState.error}
>
{getText()}
</NewDashboardButton>
</Dropdown>
</Col>
)}
</Row> </Row>
), ),
[ [
isDashboardListLoading, isDashboardListLoading,
handleSearch, handleSearch,
isFilteringDashboards, isFilteringDashboards,
searchString,
createNewDashboard,
getMenuItems, getMenuItems,
newDashboardState.loading, newDashboardState.loading,
newDashboardState.error, newDashboardState.error,
getText, getText,
searchString,
], ],
); );

View File

@ -1,171 +1,170 @@
.save-view-container { .save-view-container {
margin-top: 70px; margin-top: 70px;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
.save-view-content { .save-view-content {
width: calc(100% - 30px); width: calc(100% - 30px);
max-width: 736px; max-width: 736px;
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
}
.title { .subtitle {
color: var(--bg-vanilla-100); color: var(---bg-vanilla-400);
font-size: var(--font-size-lg); font-size: var(--font-size-sm);
font-style: normal; font-style: normal;
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
line-height: 28px; /* 155.556% */ line-height: 20px; /* 142.857% */
letter-spacing: -0.09px; letter-spacing: -0.07px;
} }
.subtitle { .ant-input-affix-wrapper {
color: var(---bg-vanilla-400); margin-top: 16px;
font-size: var(--font-size-sm); margin-bottom: 8px;
font-style: normal; }
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.ant-input-affix-wrapper { .ant-table-row {
margin-top: 16px; .ant-table-cell {
margin-bottom: 8px; padding: 0;
} border: none;
background: var(--bg-ink-500);
}
.column-render {
margin: 8px 0 !important;
padding: 16px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.ant-table-row { .title-with-action {
.ant-table-cell { display: flex;
padding: 0; justify-content: space-between;
border: none; align-items: center;
background: var(--bg-ink-500);
} .save-view-title {
.column-render { display: flex;
margin: 8px 0 !important; align-items: center;
padding: 16px; gap: 6px;
border-radius: 6px; .dot {
border: 1px solid var(--bg-slate-500); min-height: 6px;
background: var(--bg-ink-400); min-width: 6px;
border-radius: 50%;
}
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
}
}
.title-with-action { .action-btn {
display: flex; display: flex;
justify-content: space-between; align-items: center;
align-items: center; gap: 20px;
cursor: pointer;
.save-view-title { .hidden {
display: flex; display: none;
align-items: center; }
gap: 6px; }
.dot { }
min-height: 6px; .view-details {
min-width: 6px; margin-top: 8px;
border-radius: 50%; display: flex;
} align-items: center;
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
}
}
.action-btn { .view-tag {
display: flex; width: 14px;
align-items: center; height: 14px;
gap: 20px; border-radius: 50px;
cursor: pointer; background: var(--bg-slate-300);
} display: flex;
justify-content: center;
align-items: center;
} .tag-text {
.view-details { color: var(--bg-vanilla-400);
margin-top: 8px; leading-trim: both;
display: flex; text-edge: cap;
align-items: center; font-size: 10px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: normal;
letter-spacing: -0.05px;
}
}
.view-tag { .view-created-by {
width: 14px; margin-left: 8px;
height: 14px; }
border-radius: 50px;
background: var(--bg-slate-300);
display: flex;
justify-content: center;
align-items: center;
.tag-text { .view-created-at {
color: var(--bg-vanilla-400); margin-left: 24px;
leading-trim: both; display: flex;
text-edge: cap; align-items: center;
font-size: 10px; .ant-typography {
font-style: normal; margin-left: 6px;
font-weight: var(--font-weight-normal); color: var(--bg-vanilla-400);
line-height: normal; font-size: var(--font-size-sm);
letter-spacing: -0.05px; font-style: normal;
} font-weight: var(--font-weight-normal);
} line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}
}
}
.view-created-by { .ant-pagination-item {
margin-left: 8px; display: flex;
} justify-content: center;
align-items: center;
.view-created-at { > a {
margin-left: 24px; color: var(--bg-vanilla-400);
display: flex; font-variant-numeric: lining-nums tabular-nums slashed-zero;
align-items: center; font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on;
.ant-typography { font-size: var(--font-size-sm);
margin-left: 6px; font-style: normal;
color: var(--bg-vanilla-400); font-weight: var(--font-weight-normal);
font-size: var(--font-size-sm); line-height: 20px; /* 142.857% */
font-style: normal; }
font-weight: var(--font-weight-normal); }
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}
}
}
.ant-pagination-item { .ant-pagination-item-active {
background-color: var(--bg-robin-500);
display: flex; > a {
justify-content: center; color: var(--bg-ink-500) !important;
align-items: center; font-size: var(--font-size-sm);
font-style: normal;
> a { font-weight: var(--font-weight-medium);
color: var(--bg-vanilla-400); line-height: 20px;
font-variant-numeric: lining-nums tabular-nums slashed-zero; }
font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on; }
font-size: var(--font-size-sm); }
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
}
}
.ant-pagination-item-active {
background-color: var(--bg-robin-500);
> a {
color: var(--bg-ink-500) !important;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
}
}
}
} }
.delete-view-modal { .delete-view-modal {
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */ width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px; max-width: 384px;
.ant-modal-content { .ant-modal-content {
padding: 0; padding: 0;
border-radius: 4px; border-radius: 4px;
border: 1px solid var(--bg-slate-500); border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400); background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.20); box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header { .ant-modal-header {
padding: 16px; padding: 16px;
@ -177,11 +176,11 @@
.ant-typography { .ant-typography {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-style: normal; font-style: normal;
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
line-height: 20px; line-height: 20px;
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
.save-view-input { .save-view-input {
@ -211,7 +210,6 @@
} }
} }
} }
} }
.ant-modal-footer { .ant-modal-footer {
@ -223,127 +221,126 @@
.cancel-btn { .cancel-btn {
display: flex; display: flex;
align-items: center; align-items: center;
border: none; border: none;
border-radius: 2px; border-radius: 2px;
background: var(--bg-slate-500); background: var(--bg-slate-500);
} }
.delete-btn { .delete-btn {
display: flex; display: flex;
align-items: center; align-items: center;
border: none; border: none;
border-radius: 2px; border-radius: 2px;
background: var(--bg-cherry-500); background: var(--bg-cherry-500);
margin-left: 12px; margin-left: 12px;
} }
.delete-btn:hover { .delete-btn:hover {
color: var(--bg-vanilla-100); color: var(--bg-vanilla-100);
background: var(--bg-cherry-600); background: var(--bg-cherry-600);
} }
} }
} }
.title { .title {
color: var(--bg-vanilla-100); color: var(--bg-vanilla-100);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-style: normal; font-style: normal;
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */ line-height: 20px; /* 142.857% */
} }
} }
.lightMode { .lightMode {
.save-view-container { .save-view-container {
.save-view-content { .save-view-content {
.title {
color: var(--bg-ink-500);
}
.title { .ant-table-row {
color: var(--bg-ink-500); .ant-table-cell {
} background: var(--bg-vanilla-200);
}
.ant-table-row { &:hover {
.ant-table-cell { .ant-table-cell {
background: var(--bg-vanilla-200); background: var(--bg-vanilla-200) !important;
} }
}
&:hover { .column-render {
.ant-table-cell { border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-200) !important; background: var(--bg-vanilla-100);
}
}
.column-render { .title-with-action {
border: 1px solid var(--bg-vanilla-200); .save-view-title {
background: var(--bg-vanilla-100); .ant-typography {
color: var(--bg-ink-500);
}
}
.title-with-action { .action-btn {
.save-view-title { .ant-typography {
.ant-typography { color: var(--bg-ink-500);
color: var(--bg-ink-500); }
} }
} }
.action-btn { .view-details {
.ant-typography { .view-tag {
color: var(--bg-ink-500); background: var(--bg-vanilla-200);
} .tag-text {
} color: var(--bg-ink-500);
} }
}
.view-details { .view-created-by {
.view-tag { color: var(--bg-ink-500);
background: var(--bg-vanilla-200); }
.tag-text {
color: var(--bg-ink-500);
}
}
.view-created-by { .view-created-at {
color: var(--bg-ink-500); .ant-typography {
} color: var(--bg-ink-500);
}
}
}
}
}
}
}
.view-created-at { .delete-view-modal {
.ant-typography { .ant-modal-content {
color: var(--bg-ink-500); border: 1px solid var(--bg-vanilla-200);
} background: var(--bg-vanilla-100);
}
}
}
}
}
}
.delete-view-modal { .ant-modal-header {
.ant-modal-content { background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.ant-modal-header { .title {
background: var(--bg-vanilla-100); color: var(--bg-ink-500);
}
}
.title { .ant-modal-body {
color: var(--bg-ink-500); .ant-typography {
} color: var(--bg-ink-500);
} }
.ant-modal-body { .save-view-input {
.ant-typography { .ant-input {
color: var(--bg-ink-500); background: var(--bg-vanilla-200);
} color: var(--bg-ink-500);
}
}
}
.save-view-input { .ant-modal-footer {
.ant-input { .cancel-btn {
background: var(--bg-vanilla-200); background: var(--bg-vanilla-300);
color: var(--bg-ink-500); color: var(--bg-ink-400);
} }
} }
} }
}
.ant-modal-footer {
.cancel-btn {
background: var(--bg-vanilla-300);
color: var(--bg-ink-400);
}
}
}
}
} }

View File

@ -32,14 +32,20 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { ChangeEvent, useEffect, useState } from 'react'; import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { ViewProps } from 'types/api/saveViews/types'; import { ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { ROUTES_VS_SOURCEPAGE, SOURCEPAGE_VS_ROUTES } from './constants'; import { ROUTES_VS_SOURCEPAGE, SOURCEPAGE_VS_ROUTES } from './constants';
import { deleteViewHandler } from './utils'; import { deleteViewHandler } from './utils';
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
function SaveView(): JSX.Element { function SaveView(): JSX.Element {
const { pathname } = useLocation(); const { pathname } = useLocation();
const sourcepage = ROUTES_VS_SOURCEPAGE[pathname]; const sourcepage = ROUTES_VS_SOURCEPAGE[pathname];
@ -61,6 +67,8 @@ function SaveView(): JSX.Element {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
}; };
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const handleDeleteModelOpen = (uuid: string, name: string): void => { const handleDeleteModelOpen = (uuid: string, name: string): void => {
setActiveViewKey(uuid); setActiveViewKey(uuid);
setActiveViewName(name); setActiveViewName(name);
@ -217,6 +225,9 @@ function SaveView(): JSX.Element {
// Combine time and date // Combine time and date
const formattedDateAndTime = `${formattedTime}${formattedDate}`; const formattedDateAndTime = `${formattedTime}${formattedDate}`;
const isEditDeleteSupported = allowedRoles.includes(role as string);
return ( return (
<div className="column-render"> <div className="column-render">
<div className="title-with-action"> <div className="title-with-action">
@ -234,11 +245,13 @@ function SaveView(): JSX.Element {
<div className="action-btn"> <div className="action-btn">
<PenLine <PenLine
size={14} size={14}
className={isEditDeleteSupported ? '' : 'hidden'}
onClick={(): void => handleEditModelOpen(view, bgColor)} onClick={(): void => handleEditModelOpen(view, bgColor)}
/> />
<Compass size={14} onClick={(): void => handleRedirectQuery(view)} /> <Compass size={14} onClick={(): void => handleRedirectQuery(view)} />
<Trash2 <Trash2
size={14} size={14}
className={isEditDeleteSupported ? '' : 'hidden'}
color={Color.BG_CHERRY_500} color={Color.BG_CHERRY_500}
onClick={(): void => handleDeleteModelOpen(view.uuid, view.name)} onClick={(): void => handleDeleteModelOpen(view.uuid, view.name)}
/> />