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 {
position: fixed;
bottom: 16px;
@ -23,6 +26,10 @@
cursor: pointer;
}
.hidden {
display: none;
}
.ant-divider {
margin: 0;
height: 28px;
@ -55,6 +62,10 @@
.view-options,
.actions {
.hidden {
display: none;
}
display: flex;
justify-content: center;
align-items: center;
@ -102,6 +113,9 @@
}
}
}
.hidden {
display: none;
}
}
.app-content {

View File

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

View File

@ -309,49 +309,52 @@ function DashboardsList(): JSX.Element {
/>
</Col>
<Col
span={6}
style={{
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"
{createNewDashboard && (
<Col
span={6}
style={{
display: 'flex',
justifyContent: 'flex-end',
}}
>
<NewDashboardButton
icon={<PlusOutlined />}
type="primary"
data-testid="create-new-dashboard"
loading={newDashboardState.loading}
danger={newDashboardState.error}
<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"
>
{getText()}
</NewDashboardButton>
</Dropdown>
</Col>
<NewDashboardButton
icon={<PlusOutlined />}
type="primary"
data-testid="create-new-dashboard"
loading={newDashboardState.loading}
danger={newDashboardState.error}
>
{getText()}
</NewDashboardButton>
</Dropdown>
</Col>
)}
</Row>
),
[
isDashboardListLoading,
handleSearch,
isFilteringDashboards,
searchString,
createNewDashboard,
getMenuItems,
newDashboardState.loading,
newDashboardState.error,
getText,
searchString,
],
);

View File

@ -1,171 +1,170 @@
.save-view-container {
margin-top: 70px;
display: flex;
justify-content: center;
width: 100%;
margin-top: 70px;
display: flex;
justify-content: center;
width: 100%;
.save-view-content {
width: calc(100% - 30px);
max-width: 736px;
.save-view-content {
width: calc(100% - 30px);
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;
}
.subtitle {
color: var(---bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.ant-input-affix-wrapper {
margin-top: 16px;
margin-bottom: 8px;
}
.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;
}
.ant-table-row {
.ant-table-cell {
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);
.subtitle {
color: var(---bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.title-with-action {
display: flex;
justify-content: space-between;
align-items: center;
.ant-input-affix-wrapper {
margin-top: 16px;
margin-bottom: 8px;
}
.save-view-title {
display: flex;
align-items: center;
gap: 6px;
.dot {
min-height: 6px;
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;
}
}
.ant-table-row {
.ant-table-cell {
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);
.action-btn {
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
}
.title-with-action {
display: flex;
justify-content: space-between;
align-items: center;
}
.view-details {
margin-top: 8px;
display: flex;
align-items: center;
.save-view-title {
display: flex;
align-items: center;
gap: 6px;
.dot {
min-height: 6px;
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;
}
}
.view-tag {
width: 14px;
height: 14px;
border-radius: 50px;
background: var(--bg-slate-300);
display: flex;
justify-content: center;
align-items: center;
.action-btn {
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
.tag-text {
color: var(--bg-vanilla-400);
leading-trim: both;
text-edge: cap;
font-size: 10px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: normal;
letter-spacing: -0.05px;
}
}
.hidden {
display: none;
}
}
}
.view-details {
margin-top: 8px;
display: flex;
align-items: center;
.view-created-by {
margin-left: 8px;
}
.view-tag {
width: 14px;
height: 14px;
border-radius: 50px;
background: var(--bg-slate-300);
display: flex;
justify-content: center;
align-items: center;
.view-created-at {
margin-left: 24px;
display: flex;
align-items: center;
.ant-typography {
margin-left: 6px;
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}
}
}
.tag-text {
color: var(--bg-vanilla-400);
leading-trim: both;
text-edge: cap;
font-size: 10px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: normal;
letter-spacing: -0.05px;
}
}
.ant-pagination-item {
.view-created-by {
margin-left: 8px;
}
display: flex;
justify-content: center;
align-items: center;
.view-created-at {
margin-left: 24px;
display: flex;
align-items: center;
.ant-typography {
margin-left: 6px;
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}
}
}
> a {
color: var(--bg-vanilla-400);
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 {
display: flex;
justify-content: center;
align-items: center;
}
> a {
color: var(--bg-vanilla-400);
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;
}
}
}
}
.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 {
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px;
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
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 {
padding: 16px;
@ -177,11 +176,11 @@
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}
.save-view-input {
@ -211,7 +210,6 @@
}
}
}
}
.ant-modal-footer {
@ -223,127 +221,126 @@
.cancel-btn {
display: flex;
align-items: center;
border: none;
border: none;
border-radius: 2px;
background: var(--bg-slate-500);
background: var(--bg-slate-500);
}
.delete-btn {
display: flex;
.delete-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-cherry-500);
margin-left: 12px;
}
border: none;
border-radius: 2px;
background: var(--bg-cherry-500);
margin-left: 12px;
}
.delete-btn:hover {
color: var(--bg-vanilla-100);
background: var(--bg-cherry-600);
}
.delete-btn:hover {
color: var(--bg-vanilla-100);
background: var(--bg-cherry-600);
}
}
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
}
}
.lightMode {
.save-view-container {
.save-view-content {
.save-view-container {
.save-view-content {
.title {
color: var(--bg-ink-500);
}
.title {
color: var(--bg-ink-500);
}
.ant-table-row {
.ant-table-cell {
background: var(--bg-vanilla-200);
}
.ant-table-row {
.ant-table-cell {
background: var(--bg-vanilla-200);
}
&:hover {
.ant-table-cell {
background: var(--bg-vanilla-200) !important;
}
}
&:hover {
.ant-table-cell {
background: var(--bg-vanilla-200) !important;
}
}
.column-render {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.column-render {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.title-with-action {
.save-view-title {
.ant-typography {
color: var(--bg-ink-500);
}
}
.title-with-action {
.save-view-title {
.ant-typography {
color: var(--bg-ink-500);
}
}
.action-btn {
.ant-typography {
color: var(--bg-ink-500);
}
}
}
.action-btn {
.ant-typography {
color: var(--bg-ink-500);
}
}
}
.view-details {
.view-tag {
background: var(--bg-vanilla-200);
.tag-text {
color: var(--bg-ink-500);
}
}
.view-details {
.view-tag {
background: var(--bg-vanilla-200);
.tag-text {
color: var(--bg-ink-500);
}
}
.view-created-by {
color: var(--bg-ink-500);
}
.view-created-by {
color: var(--bg-ink-500);
}
.view-created-at {
.ant-typography {
color: var(--bg-ink-500);
}
}
}
}
}
}
}
.view-created-at {
.ant-typography {
color: var(--bg-ink-500);
}
}
}
}
}
}
}
.delete-view-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.delete-view-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
.title {
color: var(--bg-ink-500);
}
}
.title {
color: var(--bg-ink-500);
}
}
.ant-modal-body {
.ant-typography {
color: var(--bg-ink-500);
}
.ant-modal-body {
.ant-typography {
color: var(--bg-ink-500);
}
.save-view-input {
.ant-input {
background: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
}
.save-view-input {
.ant-input {
background: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
}
.ant-modal-footer {
.cancel-btn {
background: var(--bg-vanilla-300);
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';
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { ViewProps } from 'types/api/saveViews/types';
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 { deleteViewHandler } from './utils';
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
function SaveView(): JSX.Element {
const { pathname } = useLocation();
const sourcepage = ROUTES_VS_SOURCEPAGE[pathname];
@ -61,6 +67,8 @@ function SaveView(): JSX.Element {
setIsDeleteModalOpen(false);
};
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const handleDeleteModelOpen = (uuid: string, name: string): void => {
setActiveViewKey(uuid);
setActiveViewName(name);
@ -217,6 +225,9 @@ function SaveView(): JSX.Element {
// Combine time and date
const formattedDateAndTime = `${formattedTime}${formattedDate}`;
const isEditDeleteSupported = allowedRoles.includes(role as string);
return (
<div className="column-render">
<div className="title-with-action">
@ -234,11 +245,13 @@ function SaveView(): JSX.Element {
<div className="action-btn">
<PenLine
size={14}
className={isEditDeleteSupported ? '' : 'hidden'}
onClick={(): void => handleEditModelOpen(view, bgColor)}
/>
<Compass size={14} onClick={(): void => handleRedirectQuery(view)} />
<Trash2
size={14}
className={isEditDeleteSupported ? '' : 'hidden'}
color={Color.BG_CHERRY_500}
onClick={(): void => handleDeleteModelOpen(view.uuid, view.name)}
/>