mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-23 08:49:29 +00:00
Compare commits
3 Commits
roles-deta
...
fix/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43509681fa | ||
|
|
ff5fcc0e98 | ||
|
|
122d88c4d2 |
@@ -56,7 +56,6 @@ const ROUTES = {
|
||||
TRACE_EXPLORER: '/trace-explorer',
|
||||
BILLING: '/settings/billing',
|
||||
ROLES_SETTINGS: '/settings/roles',
|
||||
ROLE_DETAILS: '/settings/roles/:roleId',
|
||||
SUPPORT: '/support',
|
||||
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
||||
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||
|
||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -103,9 +103,10 @@ function K8sClustersList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -105,9 +105,10 @@ function K8sDaemonSetsList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -106,9 +106,10 @@ function K8sDeploymentsList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -101,9 +101,10 @@ function K8sJobsList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { safeParseJSON } from './commonUtils';
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
|
||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
||||
import { IEntityColumn } from './utils';
|
||||
@@ -58,9 +59,10 @@ function K8sHeader({
|
||||
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
|
||||
let { filters } = currentQuery.builder.queryData[0];
|
||||
if (urlFilters) {
|
||||
const decoded = decodeURIComponent(urlFilters);
|
||||
const parsed = JSON.parse(decoded);
|
||||
filters = parsed;
|
||||
const parsed = safeParseJSON<IBuilderQuery['filters']>(urlFilters);
|
||||
if (parsed) {
|
||||
filters = parsed;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...currentQuery,
|
||||
|
||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -104,9 +104,10 @@ function K8sNamespacesList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -99,9 +99,10 @@ function K8sNodesList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -92,9 +92,10 @@ function K8sPodsList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -105,9 +105,10 @@ function K8sStatefulSetsList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { getOrderByFromParams } from '../commonUtils';
|
||||
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
@@ -105,9 +105,10 @@ function K8sVolumesList({
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
if (groupBy) {
|
||||
const decoded = decodeURIComponent(groupBy);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['groupBy'];
|
||||
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Table, Tooltip, Typography } from 'antd';
|
||||
import { Progress } from 'antd/lib';
|
||||
@@ -260,6 +261,19 @@ export const filterDuplicateFilters = (
|
||||
return uniqueFilters;
|
||||
};
|
||||
|
||||
export const safeParseJSON = <T,>(value: string): T | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch (e) {
|
||||
console.error('Error parsing JSON from URL parameter:', e);
|
||||
// TODO: Should we capture this error in Sentry?
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrderByFromParams = (
|
||||
searchParams: URLSearchParams,
|
||||
returnNullAsDefault = false,
|
||||
@@ -271,9 +285,12 @@ export const getOrderByFromParams = (
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||
);
|
||||
if (orderByFromParams) {
|
||||
const decoded = decodeURIComponent(orderByFromParams);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as { columnName: string; order: 'asc' | 'desc' };
|
||||
const parsed = safeParseJSON<{ columnName: string; order: 'asc' | 'desc' }>(
|
||||
orderByFromParams,
|
||||
);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
if (returnNullAsDefault) {
|
||||
return null;
|
||||
@@ -287,13 +304,7 @@ export const getFiltersFromParams = (
|
||||
): IBuilderQuery['filters'] | null => {
|
||||
const filtersFromParams = searchParams.get(queryKey);
|
||||
if (filtersFromParams) {
|
||||
try {
|
||||
const decoded = decodeURIComponent(filtersFromParams);
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed as IBuilderQuery['filters'];
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return safeParseJSON<IBuilderQuery['filters']>(filtersFromParams);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
.permission-side-panel-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.permission-side-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 101;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-ink-400);
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
background: var(--bg-ink-400);
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
&__close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--foreground);
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-base-white);
|
||||
}
|
||||
}
|
||||
|
||||
&__header-divider {
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: var(--bg-slate-500);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
&__resource-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
gap: 12px;
|
||||
background: var(--bg-ink-400);
|
||||
border-top: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
&__unsaved {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&__unsaved-dot {
|
||||
display: block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50px;
|
||||
background: var(--bg-robin-500);
|
||||
box-shadow: 0px 0px 6px 0px rgba(78, 116, 248, 0.4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__unsaved-text {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
|
||||
&__footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__cancel-btn {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 4px !important;
|
||||
padding: 4px 8px !important;
|
||||
height: 32px !important;
|
||||
background: var(--bg-slate-500) !important;
|
||||
border: none !important;
|
||||
border-radius: 2px !important;
|
||||
font-family: Inter !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
line-height: 24px !important;
|
||||
color: var(--foreground) !important;
|
||||
cursor: pointer !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-slate-400) !important;
|
||||
color: var(--text-base-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.psp-resource {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&--expanded {
|
||||
background: rgba(171, 189, 255, 0.04);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(171, 189, 255, 0.03);
|
||||
}
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--text-base-white);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__switch-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Expanded body
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 0 8px 44px;
|
||||
background: rgba(171, 189, 255, 0.04);
|
||||
}
|
||||
|
||||
&__radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
|
||||
// RadioGroupLabel style
|
||||
label {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--text-base-white);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__select-wrapper {
|
||||
padding: 6px 16px 4px 24px;
|
||||
}
|
||||
|
||||
&__select {
|
||||
width: 100%;
|
||||
|
||||
.ant-select-selector {
|
||||
background: var(--bg-ink-300) !important;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
border-radius: 2px !important;
|
||||
padding: 4px 6px !important;
|
||||
min-height: 32px !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
border-color: var(--input) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--foreground);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
background: var(--bg-slate-300) !important;
|
||||
border: none !important;
|
||||
border-radius: 2px !important;
|
||||
padding: 0 6px !important;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: var(--text-base-white) !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item-remove {
|
||||
color: var(--foreground) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__select-popup {
|
||||
.ant-select-item {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--foreground);
|
||||
background: var(--bg-ink-300);
|
||||
|
||||
&-option-selected {
|
||||
background: var(--bg-slate-400) !important;
|
||||
color: var(--text-base-white) !important;
|
||||
}
|
||||
|
||||
&-option-active {
|
||||
background: rgba(171, 189, 255, 0.06) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-dropdown {
|
||||
background: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 2px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.permission-side-panel {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-left-color: var(--bg-vanilla-300);
|
||||
|
||||
&__header {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-bottom-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&__header-divider {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&__footer {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-top-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&__cancel-btn {
|
||||
background: var(--bg-vanilla-300) !important;
|
||||
color: var(--bg-ink-400) !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-vanilla-400) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.psp-resource {
|
||||
border-bottom-color: var(--bg-vanilla-300);
|
||||
|
||||
&__label {
|
||||
color: var(--text-base-black);
|
||||
}
|
||||
|
||||
&__radio-item label {
|
||||
color: var(--text-base-black);
|
||||
}
|
||||
|
||||
&__select .ant-select-selector {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
&__select .ant-select-selection-item {
|
||||
background: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-base-black) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-side-panel__resource-list {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
RadioGroupLabel,
|
||||
} from '@signozhq/radio-group';
|
||||
import { Switch } from '@signozhq/switch';
|
||||
import { Select } from 'antd';
|
||||
|
||||
import type {
|
||||
PermissionConfig,
|
||||
PermissionSidePanelProps,
|
||||
ResourceConfig,
|
||||
ResourceDefinition,
|
||||
ScopeType,
|
||||
} from './PermissionSidePanel.types';
|
||||
|
||||
import './PermissionSidePanel.styles.scss';
|
||||
|
||||
const DEFAULT_RESOURCE_CONFIG: ResourceConfig = {
|
||||
enabled: false,
|
||||
scope: 'all',
|
||||
selectedIds: [],
|
||||
};
|
||||
|
||||
function buildConfig(
|
||||
resources: ResourceDefinition[],
|
||||
initial?: PermissionConfig,
|
||||
): PermissionConfig {
|
||||
const config: PermissionConfig = {};
|
||||
resources.forEach((r) => {
|
||||
config[r.id] = initial?.[r.id] ?? { ...DEFAULT_RESOURCE_CONFIG };
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
function configsEqual(a: PermissionConfig, b: PermissionConfig): boolean {
|
||||
return Object.keys(a).every((id) => {
|
||||
const ac = a[id];
|
||||
const bc = b[id];
|
||||
if (!bc) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
ac.enabled === bc.enabled &&
|
||||
ac.scope === bc.scope &&
|
||||
JSON.stringify([...ac.selectedIds].sort()) ===
|
||||
JSON.stringify([...bc.selectedIds].sort())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
interface ResourceRowProps {
|
||||
resource: ResourceDefinition;
|
||||
config: ResourceConfig;
|
||||
isExpanded: boolean;
|
||||
onToggle: (id: string, checked: boolean) => void;
|
||||
onToggleExpand: (id: string) => void;
|
||||
onScopeChange: (id: string, scope: ScopeType) => void;
|
||||
onSelectedIdsChange: (id: string, ids: string[]) => void;
|
||||
}
|
||||
|
||||
function ResourceRow({
|
||||
resource,
|
||||
config,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
onToggleExpand,
|
||||
onScopeChange,
|
||||
onSelectedIdsChange,
|
||||
}: ResourceRowProps): JSX.Element {
|
||||
return (
|
||||
<div className="psp-resource">
|
||||
<div
|
||||
className={`psp-resource__row${
|
||||
isExpanded ? ' psp-resource__row--expanded' : ''
|
||||
}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => onToggleExpand(resource.id)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onToggleExpand(resource.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="psp-resource__left">
|
||||
<span className="psp-resource__chevron">
|
||||
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</span>
|
||||
<span className="psp-resource__label">{resource.label}</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="psp-resource__switch-wrapper"
|
||||
role="presentation"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
onKeyDown={(e): void => e.stopPropagation()}
|
||||
>
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onCheckedChange={(checked): void => onToggle(resource.id, checked)}
|
||||
color="robin"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="psp-resource__body">
|
||||
<RadioGroup
|
||||
value={config.scope}
|
||||
onValueChange={(val): void =>
|
||||
onScopeChange(resource.id, val as ScopeType)
|
||||
}
|
||||
className="psp-resource__radio-group"
|
||||
>
|
||||
<div className="psp-resource__radio-item">
|
||||
<RadioGroupItem value="all" id={`${resource.id}-all`} color="robin" />
|
||||
<RadioGroupLabel htmlFor={`${resource.id}-all`}>All</RadioGroupLabel>
|
||||
</div>
|
||||
|
||||
<div className="psp-resource__radio-item">
|
||||
<RadioGroupItem
|
||||
value="only_selected"
|
||||
id={`${resource.id}-only-selected`}
|
||||
color="robin"
|
||||
/>
|
||||
<RadioGroupLabel htmlFor={`${resource.id}-only-selected`}>
|
||||
Only selected
|
||||
</RadioGroupLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{config.scope === 'only_selected' && (
|
||||
<div className="psp-resource__select-wrapper">
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={config.selectedIds}
|
||||
onChange={(vals: string[]): void =>
|
||||
onSelectedIdsChange(resource.id, vals)
|
||||
}
|
||||
options={resource.options ?? []}
|
||||
placeholder="Select resources..."
|
||||
className="psp-resource__select"
|
||||
popupClassName="psp-resource__select-popup"
|
||||
showSearch
|
||||
filterOption={(input, option): boolean =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PermissionSidePanel({
|
||||
open,
|
||||
onClose,
|
||||
permissionLabel,
|
||||
resources,
|
||||
initialConfig,
|
||||
onSave,
|
||||
}: PermissionSidePanelProps): JSX.Element | null {
|
||||
const [config, setConfig] = useState<PermissionConfig>(() =>
|
||||
buildConfig(resources, initialConfig),
|
||||
);
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setConfig(buildConfig(resources, initialConfig));
|
||||
setExpandedIds(new Set());
|
||||
}
|
||||
}, [open, resources, initialConfig]);
|
||||
|
||||
const savedConfig = useMemo(() => buildConfig(resources, initialConfig), [
|
||||
resources,
|
||||
initialConfig,
|
||||
]);
|
||||
|
||||
const unsavedCount = useMemo(() => {
|
||||
if (configsEqual(config, savedConfig)) {
|
||||
return 0;
|
||||
}
|
||||
return Object.keys(config).filter((id) => {
|
||||
const a = config[id];
|
||||
const b = savedConfig[id];
|
||||
if (!b) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
a.enabled !== b.enabled ||
|
||||
a.scope !== b.scope ||
|
||||
JSON.stringify([...a.selectedIds].sort()) !==
|
||||
JSON.stringify([...b.selectedIds].sort())
|
||||
);
|
||||
}).length;
|
||||
}, [config, savedConfig]);
|
||||
|
||||
const updateResource = useCallback(
|
||||
(id: string, patch: Partial<ResourceConfig>): void => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
[id]: { ...prev[id], ...patch },
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(id: string, checked: boolean): void => {
|
||||
updateResource(id, { enabled: checked });
|
||||
},
|
||||
[updateResource],
|
||||
);
|
||||
|
||||
const handleToggleExpand = useCallback((id: string): void => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleScopeChange = useCallback(
|
||||
(id: string, scope: ScopeType): void => {
|
||||
updateResource(id, { scope, selectedIds: [] });
|
||||
},
|
||||
[updateResource],
|
||||
);
|
||||
|
||||
const handleSelectedIdsChange = useCallback(
|
||||
(id: string, ids: string[]): void => {
|
||||
updateResource(id, { selectedIds: ids });
|
||||
},
|
||||
[updateResource],
|
||||
);
|
||||
|
||||
const handleSave = useCallback((): void => {
|
||||
onSave(config);
|
||||
onClose();
|
||||
}, [config, onSave, onClose]);
|
||||
|
||||
const handleDiscard = useCallback((): void => {
|
||||
setConfig(buildConfig(resources, initialConfig));
|
||||
setExpandedIds(new Set());
|
||||
}, [resources, initialConfig]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="permission-side-panel-backdrop"
|
||||
role="presentation"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<div className="permission-side-panel">
|
||||
<div className="permission-side-panel__header">
|
||||
<button
|
||||
type="button"
|
||||
className="permission-side-panel__close"
|
||||
onClick={onClose}
|
||||
aria-label="Close panel"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
<span className="permission-side-panel__header-divider" />
|
||||
<span className="permission-side-panel__title">
|
||||
Edit {permissionLabel} Permissions
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="permission-side-panel__content">
|
||||
<div className="permission-side-panel__resource-list">
|
||||
{resources.map((resource) => (
|
||||
<ResourceRow
|
||||
key={resource.id}
|
||||
resource={resource}
|
||||
config={config[resource.id] ?? DEFAULT_RESOURCE_CONFIG}
|
||||
isExpanded={expandedIds.has(resource.id)}
|
||||
onToggle={handleToggle}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
onScopeChange={handleScopeChange}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="permission-side-panel__footer">
|
||||
{unsavedCount > 0 && (
|
||||
<div className="permission-side-panel__unsaved">
|
||||
<span className="permission-side-panel__unsaved-dot" />
|
||||
<span className="permission-side-panel__unsaved-text">
|
||||
{unsavedCount} unsaved change{unsavedCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="permission-side-panel__footer-actions">
|
||||
<Button
|
||||
className="permission-side-panel__cancel-btn"
|
||||
prefixIcon={<X size={14} />}
|
||||
onClick={unsavedCount > 0 ? handleDiscard : onClose}
|
||||
size="sm"
|
||||
>
|
||||
{unsavedCount > 0 ? 'Discard' : 'Cancel'}
|
||||
</Button>
|
||||
<Button variant="solid" color="primary" size="sm" onClick={handleSave}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PermissionSidePanel;
|
||||
@@ -1,35 +0,0 @@
|
||||
export interface ResourceOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ResourceDefinition {
|
||||
id: string;
|
||||
label: string;
|
||||
/** Options for the "Only selected" dropdown — to be populated via API */
|
||||
options?: ResourceOption[];
|
||||
}
|
||||
|
||||
export type ScopeType = 'all' | 'only_selected';
|
||||
|
||||
export interface ResourceConfig {
|
||||
enabled: boolean;
|
||||
scope: ScopeType;
|
||||
selectedIds: string[];
|
||||
}
|
||||
|
||||
/** keyed by ResourceDefinition.id */
|
||||
export type PermissionConfig = Record<string, ResourceConfig>;
|
||||
|
||||
export interface PermissionSidePanelProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
/** e.g. "Read", "Create", "Delete" */
|
||||
permissionLabel: string;
|
||||
/** Ordered list of resources shown in the panel */
|
||||
resources: ResourceDefinition[];
|
||||
/** Pre-existing configuration to initialise from */
|
||||
initialConfig?: PermissionConfig;
|
||||
/** Called with the full resolved config when user saves */
|
||||
onSave: (config: PermissionConfig) => void;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export { default } from './PermissionSidePanel';
|
||||
export type {
|
||||
PermissionConfig,
|
||||
PermissionSidePanelProps,
|
||||
ResourceConfig,
|
||||
ResourceDefinition,
|
||||
ResourceOption,
|
||||
ScopeType,
|
||||
} from './PermissionSidePanel.types';
|
||||
@@ -1,500 +0,0 @@
|
||||
.role-details-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
|
||||
.role-details-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.role-details-title {
|
||||
color: var(--text-base-white);
|
||||
font-family: Inter;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.09px;
|
||||
}
|
||||
|
||||
.role-details-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.role-details-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.role-details-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
color: var(--foreground);
|
||||
background: transparent;
|
||||
border: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&--active {
|
||||
background: var(--bg-slate-400);
|
||||
color: var(--text-base-white);
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-tab-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 50px;
|
||||
background: var(--bg-slate-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: var(--foreground);
|
||||
letter-spacing: -0.06px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.role-details-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.role-details-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.role-details-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.role-details-section-label {
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.48px;
|
||||
text-transform: uppercase;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.role-details-description-text {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.role-details-info-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.role-details-info-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.role-details-info-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.role-details-info-name {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.role-details-permissions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.role-details-permissions-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.role-details-permissions-divider {
|
||||
flex: 1;
|
||||
border: none;
|
||||
border-top: 2px dotted var(--bg-slate-300);
|
||||
border-bottom: 2px dotted var(--bg-slate-300);
|
||||
height: 7px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.role-details-permission-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.role-details-permission-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
background: rgba(171, 189, 255, 0.08);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(171, 189, 255, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-permission-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.role-details-permission-item-label {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--text-base-white);
|
||||
}
|
||||
|
||||
.role-details-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-slate-400);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.role-details-members {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.role-details-members-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 6px 6px 6px 8px;
|
||||
background: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 2px;
|
||||
|
||||
.role-details-members-search-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--foreground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.role-details-members-search-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--foreground);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--foreground);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-members-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 420px;
|
||||
border: 1px dashed var(--bg-slate-500);
|
||||
border-radius: 3px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.role-details-members-empty-state {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 48px 0;
|
||||
flex-grow: 1;
|
||||
|
||||
.role-details-members-empty-emoji {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.role-details-members-empty-text {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
&--bold {
|
||||
font-weight: 500;
|
||||
color: var(--text-base-white);
|
||||
}
|
||||
|
||||
&--muted {
|
||||
font-weight: 400;
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-skeleton {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-delete-action-btn {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-width: 32px !important;
|
||||
border: none !important;
|
||||
border-radius: 2px !important;
|
||||
background: transparent !important;
|
||||
color: var(--bg-cherry-500) !important;
|
||||
opacity: 0.6 !important;
|
||||
padding: 0 !important;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
box-shadow: none !important;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 72, 77, 0.1) !important;
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-delete-modal {
|
||||
width: calc(100% - 30px) !important;
|
||||
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.2);
|
||||
|
||||
.ant-modal-header {
|
||||
padding: 16px;
|
||||
background: var(--bg-ink-400);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0 16px 28px 16px;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
.cancel-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-cherry-500);
|
||||
margin-left: 12px;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-vanilla-100);
|
||||
background: var(--bg-cherry-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
}
|
||||
|
||||
.delete-text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.role-details-delete-modal {
|
||||
.ant-modal-content {
|
||||
border-color: var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.delete-text {
|
||||
color: var(--bg-ink-500);
|
||||
|
||||
strong {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
.cancel-btn {
|
||||
background: var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.role-details-page {
|
||||
.role-details-title {
|
||||
color: var(--text-base-black);
|
||||
}
|
||||
|
||||
.role-details-members-search {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.role-details-members-content {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.role-details-members-empty-state {
|
||||
.role-details-members-empty-text--bold {
|
||||
color: var(--text-base-black);
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-tabs {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.role-details-tab--active {
|
||||
background: var(--bg-vanilla-300);
|
||||
color: var(--text-base-black);
|
||||
}
|
||||
|
||||
.role-details-tab-count {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.role-details-permission-item {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-permission-item-label {
|
||||
color: var(--text-base-black);
|
||||
}
|
||||
|
||||
.role-details-badge {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Modal, Skeleton } from 'antd';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { useDeleteRole, useGetRole } from 'api/generated/services/role';
|
||||
import { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
BadgePlus,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
LayoutList,
|
||||
PencilRuler,
|
||||
Search,
|
||||
Table2,
|
||||
Trash2,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import type {
|
||||
PermissionConfig,
|
||||
ResourceDefinition,
|
||||
} from '../PermissionSidePanel';
|
||||
import PermissionSidePanel from '../PermissionSidePanel';
|
||||
import CreateRoleModal from '../RolesComponents/CreateRoleModal';
|
||||
|
||||
import './RoleDetailsPage.styles.scss';
|
||||
|
||||
// Placeholder resources — replace with API-driven data when integrating
|
||||
const PERMISSION_RESOURCES: ResourceDefinition[] = [
|
||||
{ id: 'dashboards', label: 'Dashboards' },
|
||||
{ id: 'alerts', label: 'Alerts' },
|
||||
{ id: 'logs_pipelines', label: 'Logs: Pipelines' },
|
||||
{ id: 'logs_views', label: 'Logs: Views' },
|
||||
{ id: 'traces_funnels', label: 'Traces: Funnels' },
|
||||
{ id: 'traces_views', label: 'Traces: Views' },
|
||||
{ id: 'integrations', label: 'Integrations' },
|
||||
{ id: 'exceptions', label: 'Exceptions' },
|
||||
];
|
||||
|
||||
type TabKey = 'overview' | 'members';
|
||||
|
||||
interface PermissionType {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
|
||||
const PERMISSION_TYPES: PermissionType[] = [
|
||||
{ key: 'create', label: 'Create', icon: <BadgePlus size={14} /> },
|
||||
{ key: 'list', label: 'List', icon: <LayoutList size={14} /> },
|
||||
{ key: 'read', label: 'Read', icon: <Eye size={14} /> },
|
||||
{ key: 'update', label: 'Update', icon: <PencilRuler size={14} /> },
|
||||
{ key: 'delete', label: 'Delete', icon: <Trash2 size={14} /> },
|
||||
];
|
||||
|
||||
interface OverviewTabProps {
|
||||
role: RoletypesRoleDTO;
|
||||
onPermissionClick: (permissionLabel: string) => void;
|
||||
}
|
||||
|
||||
function TimestampBadge({ date }: { date?: Date | string }): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
if (!date) {
|
||||
return <span className="role-details-badge">—</span>;
|
||||
}
|
||||
|
||||
const d = new Date(date);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return <span className="role-details-badge">—</span>;
|
||||
}
|
||||
|
||||
const formatted = formatTimezoneAdjustedTimestamp(
|
||||
date,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME,
|
||||
);
|
||||
|
||||
return <span className="role-details-badge">{formatted}</span>;
|
||||
}
|
||||
|
||||
function OverviewTab({
|
||||
role,
|
||||
onPermissionClick,
|
||||
}: OverviewTabProps): JSX.Element {
|
||||
return (
|
||||
<div className="role-details-overview">
|
||||
<div className="role-details-meta">
|
||||
<div>
|
||||
<p className="role-details-section-label">Description</p>
|
||||
<p className="role-details-description-text">{role.description || '—'}</p>
|
||||
</div>
|
||||
|
||||
<div className="role-details-info-row">
|
||||
<div className="role-details-info-col">
|
||||
<p className="role-details-section-label">Created At</p>
|
||||
<div className="role-details-info-value">
|
||||
<TimestampBadge date={role.createdAt} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="role-details-info-col">
|
||||
<p className="role-details-section-label">Last Modified At</p>
|
||||
<div className="role-details-info-value">
|
||||
<TimestampBadge date={role.updatedAt} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="role-details-permissions">
|
||||
<div className="role-details-permissions-header">
|
||||
<span className="role-details-section-label">Permissions</span>
|
||||
<hr className="role-details-permissions-divider" />
|
||||
</div>
|
||||
|
||||
<div className="role-details-permission-list">
|
||||
{PERMISSION_TYPES.map(({ key, label, icon }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="role-details-permission-item"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => onPermissionClick(label)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onPermissionClick(label);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="role-details-permission-item-left">
|
||||
{icon}
|
||||
<span className="role-details-permission-item-label">{label}</span>
|
||||
</div>
|
||||
<ChevronRight size={14} color="var(--foreground)" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MembersTab(): JSX.Element {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
return (
|
||||
<div className="role-details-members">
|
||||
<div className="role-details-members-search">
|
||||
<Search size={12} className="role-details-members-search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
className="role-details-members-search-input"
|
||||
placeholder="Search and add members..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="role-details-members-content">
|
||||
<div className="role-details-members-empty-state">
|
||||
<span
|
||||
className="role-details-members-empty-emoji"
|
||||
role="img"
|
||||
aria-label="monocle face"
|
||||
>
|
||||
🧐
|
||||
</span>
|
||||
<p className="role-details-members-empty-text">
|
||||
<span className="role-details-members-empty-text--bold">
|
||||
No members added.
|
||||
</span>{' '}
|
||||
<span className="role-details-members-empty-text--muted">
|
||||
Start adding members to this role.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoleDetailsPage(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
// Extract roleId from pathname — useParams doesn't work inside nested RouteTab (antd Tabs) routing
|
||||
const roleIdMatch = pathname.match(/\/settings\/roles\/([^/]+)/);
|
||||
const roleId = roleIdMatch ? roleIdMatch[1] : '';
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('overview');
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [activePermission, setActivePermission] = useState<string | null>(null);
|
||||
const [permissionConfigs, setPermissionConfigs] = useState<
|
||||
Record<string, PermissionConfig>
|
||||
>({});
|
||||
|
||||
const { data, isLoading, isError, error } = useGetRole({ id: roleId });
|
||||
const role = data?.data?.data;
|
||||
|
||||
const { mutate: deleteRole, isLoading: isDeleting } = useDeleteRole({
|
||||
mutation: {
|
||||
onSuccess: (): void => {
|
||||
toast.success('Role deleted successfully');
|
||||
history.push(ROUTES.ROLES_SETTINGS);
|
||||
},
|
||||
onError: (err): void => {
|
||||
try {
|
||||
ErrorResponseHandlerV2(err as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const openEditModal = (): void => {
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="role-details-page">
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{ rows: 8 }}
|
||||
className="role-details-skeleton"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !role) {
|
||||
return (
|
||||
<div className="role-details-page">
|
||||
<ErrorInPlace
|
||||
error={toAPIError(
|
||||
error,
|
||||
'An unexpected error occurred while fetching role details.',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="role-details-page">
|
||||
{/* Header */}
|
||||
<div className="role-details-header">
|
||||
<h2 className="role-details-title">Role — {role.name}</h2>
|
||||
</div>
|
||||
|
||||
{/* Tab bar + Actions */}
|
||||
<div className="role-details-nav">
|
||||
<div className="role-details-tabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`role-details-tab${
|
||||
activeTab === 'overview' ? ' role-details-tab--active' : ''
|
||||
}`}
|
||||
onClick={(): void => setActiveTab('overview')}
|
||||
>
|
||||
<Table2 size={14} />
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`role-details-tab${
|
||||
activeTab === 'members' ? ' role-details-tab--active' : ''
|
||||
}`}
|
||||
onClick={(): void => setActiveTab('members')}
|
||||
>
|
||||
<Users size={14} />
|
||||
Members
|
||||
<span className="role-details-tab-count">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="role-details-actions">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className="role-details-delete-action-btn"
|
||||
onClick={(): void => setIsDeleteModalOpen(true)}
|
||||
aria-label="Delete role"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={openEditModal}
|
||||
>
|
||||
Edit Role Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{activeTab === 'overview' && (
|
||||
<OverviewTab
|
||||
role={role}
|
||||
onPermissionClick={(label): void => setActivePermission(label)}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'members' && <MembersTab />}
|
||||
|
||||
<PermissionSidePanel
|
||||
open={activePermission !== null}
|
||||
onClose={(): void => setActivePermission(null)}
|
||||
permissionLabel={activePermission ?? ''}
|
||||
resources={PERMISSION_RESOURCES}
|
||||
initialConfig={
|
||||
activePermission ? permissionConfigs[activePermission] : undefined
|
||||
}
|
||||
onSave={(config): void => {
|
||||
if (activePermission) {
|
||||
setPermissionConfigs((prev) => ({
|
||||
...prev,
|
||||
[activePermission]: config,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Edit Role Modal */}
|
||||
<CreateRoleModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={(): void => setIsEditModalOpen(false)}
|
||||
initialData={{
|
||||
id: roleId,
|
||||
name: role.name || '',
|
||||
description: role.description || '',
|
||||
}}
|
||||
/>
|
||||
{/* Delete Role Confirmation Modal */}
|
||||
<Modal
|
||||
open={isDeleteModalOpen}
|
||||
onCancel={(): void => setIsDeleteModalOpen(false)}
|
||||
title={<span className="title">Delete Role</span>}
|
||||
closable
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
className="cancel-btn"
|
||||
prefixIcon={<X size={16} />}
|
||||
onClick={(): void => setIsDeleteModalOpen(false)}
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
className="delete-btn"
|
||||
prefixIcon={<Trash2 size={16} />}
|
||||
onClick={(): void => deleteRole({ pathParams: { id: roleId } })}
|
||||
loading={isDeleting}
|
||||
size="sm"
|
||||
>
|
||||
Delete Role
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnClose
|
||||
className="role-details-delete-modal"
|
||||
>
|
||||
<p className="delete-text">
|
||||
Are you sure you want to delete the role <strong>{role.name}</strong>? This
|
||||
action cannot be undone.
|
||||
</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoleDetailsPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './RoleDetailsPage';
|
||||
@@ -1,176 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Input, inputVariants } from '@signozhq/input';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Form, Modal } from 'antd';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import {
|
||||
invalidateGetRole,
|
||||
invalidateListRoles,
|
||||
useCreateRole,
|
||||
usePatchRole,
|
||||
} from 'api/generated/services/role';
|
||||
import type { RoletypesPostableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import '../RolesSettings.styles.scss';
|
||||
|
||||
export interface CreateRoleModalInitialData {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface CreateRoleModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
initialData?: CreateRoleModalInitialData;
|
||||
}
|
||||
|
||||
interface CreateRoleFormValues {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
function CreateRoleModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
initialData,
|
||||
}: CreateRoleModalProps): JSX.Element {
|
||||
const [form] = Form.useForm<CreateRoleFormValues>();
|
||||
const queryClient = useQueryClient();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const isEditMode = !!initialData?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (isEditMode && initialData) {
|
||||
form.setFieldsValue({
|
||||
name: initialData.name,
|
||||
description: initialData.description || '',
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}
|
||||
}, [isOpen, isEditMode, initialData, form]);
|
||||
|
||||
const handleSuccess = async (message: string): Promise<void> => {
|
||||
await invalidateListRoles(queryClient);
|
||||
if (isEditMode && initialData?.id) {
|
||||
await invalidateGetRole(queryClient, { id: initialData.id });
|
||||
}
|
||||
toast.success(message);
|
||||
form.resetFields();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleError = (error: unknown): void => {
|
||||
try {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
};
|
||||
|
||||
const { mutate: createRole, isLoading: isCreating } = useCreateRole({
|
||||
mutation: {
|
||||
onSuccess: () => handleSuccess('Role created successfully'),
|
||||
onError: handleError,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: patchRole, isLoading: isPatching } = usePatchRole({
|
||||
mutation: {
|
||||
onSuccess: () => handleSuccess('Role updated successfully'),
|
||||
onError: handleError,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (isEditMode && initialData?.id) {
|
||||
patchRole({
|
||||
pathParams: { id: initialData.id },
|
||||
data: { description: values.description || '' },
|
||||
});
|
||||
} else {
|
||||
const data: RoletypesPostableRoleDTO = {
|
||||
name: values.name,
|
||||
...(values.description ? { description: values.description } : {}),
|
||||
};
|
||||
createRole({ data });
|
||||
}
|
||||
} catch {
|
||||
// form validation failed; antd handles inline error display
|
||||
}
|
||||
}, [form, createRole, patchRole, isEditMode, initialData]);
|
||||
|
||||
const onCancel = useCallback((): void => {
|
||||
form.resetFields();
|
||||
onClose();
|
||||
}, [form, onClose]);
|
||||
|
||||
const isLoading = isCreating || isPatching;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onCancel={onCancel}
|
||||
title={isEditMode ? 'Edit Role Details' : 'Create a New Role'}
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
size="sm"
|
||||
>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
loading={isLoading}
|
||||
size="sm"
|
||||
>
|
||||
{isEditMode ? 'Save Changes' : 'Create Role'}
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnClose
|
||||
className="create-role-modal"
|
||||
width={530}
|
||||
>
|
||||
<Form form={form} layout="vertical" className="create-role-form">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{ required: true, message: 'Role name is required' }]}
|
||||
>
|
||||
<Input
|
||||
disabled={isEditMode}
|
||||
placeholder="Enter role name e.g. : Service Owner"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="Description">
|
||||
<textarea
|
||||
className={inputVariants()}
|
||||
placeholder="A helpful description of the role"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateRoleModal;
|
||||
@@ -5,7 +5,6 @@ import { useListRoles } from 'api/generated/services/role';
|
||||
import { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -175,28 +174,9 @@ function RolesListingTable({
|
||||
);
|
||||
}
|
||||
|
||||
const navigateToRole = (roleId: string): void => {
|
||||
history.push(ROUTES.ROLE_DETAILS.replace(':roleId', roleId));
|
||||
};
|
||||
|
||||
// todo: use table from periscope when its available for consumption
|
||||
const renderRow = (role: RoletypesRoleDTO): JSX.Element => (
|
||||
<div
|
||||
key={role.id}
|
||||
className="roles-table-row roles-table-row--clickable"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => {
|
||||
if (role.id) {
|
||||
navigateToRole(role.id);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && role.id) {
|
||||
navigateToRole(role.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div key={role.id} className="roles-table-row">
|
||||
<div className="roles-table-cell roles-table-cell--name">
|
||||
{role.name ?? '—'}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
.roles-settings-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
@@ -32,28 +31,8 @@
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.roles-settings-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.role-settings-toolbar-button {
|
||||
display: flex;
|
||||
width: 156px;
|
||||
height: 32px;
|
||||
padding: 6px 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: https://github.com/SigNoz/components/issues/116
|
||||
.roles-search-wrapper {
|
||||
flex: 1;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
background: var(--l3-background);
|
||||
@@ -174,14 +153,6 @@
|
||||
background: rgba(171, 189, 255, 0.02);
|
||||
border-bottom: 1px solid var(--secondary);
|
||||
gap: 24px;
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(171, 189, 255, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.roles-table-cell {
|
||||
@@ -265,151 +236,3 @@
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
.create-role-modal {
|
||||
.ant-modal-content {
|
||||
padding: 0;
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--bg-ink-400);
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
top: 14px;
|
||||
inset-inline-end: 16px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--foreground);
|
||||
|
||||
.ant-modal-close-x {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--text-base-white);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.create-role-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-form-item-label {
|
||||
padding-bottom: 8px;
|
||||
|
||||
label {
|
||||
color: var(--foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: https://github.com/SigNoz/components/issues/116
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l3-border);
|
||||
border-radius: 2px;
|
||||
padding: 6px 8px;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--l1-foreground);
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: var(--input);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
border-top: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.create-role-modal {
|
||||
.ant-modal-content {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-bottom-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--text-base-black);
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
border-top-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
|
||||
import CreateRoleModal from './RolesComponents/CreateRoleModal';
|
||||
import RolesListingTable from './RolesComponents/RolesListingTable';
|
||||
|
||||
import './RolesSettings.styles.scss';
|
||||
|
||||
function RolesSettings(): JSX.Element {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="roles-settings" data-testid="roles-settings">
|
||||
@@ -21,31 +17,16 @@ function RolesSettings(): JSX.Element {
|
||||
</p>
|
||||
</div>
|
||||
<div className="roles-settings-content">
|
||||
<div className="roles-settings-toolbar">
|
||||
<div className="roles-search-wrapper">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search for roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className="role-settings-toolbar-button"
|
||||
onClick={(): void => setIsCreateModalOpen(true)}
|
||||
>
|
||||
<Plus size={14} />
|
||||
Custom role
|
||||
</Button>
|
||||
<div className="roles-search-wrapper">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search for roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<RolesListingTable searchQuery={searchQuery} />
|
||||
</div>
|
||||
<CreateRoleModal
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={(): void => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,6 @@ export const routesToSkip = [
|
||||
ROUTES.LOGS_PIPELINES,
|
||||
ROUTES.BILLING,
|
||||
ROUTES.ROLES_SETTINGS,
|
||||
ROUTES.ROLE_DETAILS,
|
||||
ROUTES.SUPPORT,
|
||||
ROUTES.WORKSPACE_LOCKED,
|
||||
ROUTES.WORKSPACE_SUSPENDED,
|
||||
|
||||
@@ -78,7 +78,6 @@ function SettingsPage(): JSX.Element {
|
||||
isEnabled:
|
||||
item.key === ROUTES.BILLING ||
|
||||
item.key === ROUTES.ROLES_SETTINGS ||
|
||||
item.key === ROUTES.ROLE_DETAILS ||
|
||||
item.key === ROUTES.INTEGRATIONS ||
|
||||
item.key === ROUTES.CUSTOM_DOMAIN_SETTINGS ||
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
@@ -110,7 +109,6 @@ function SettingsPage(): JSX.Element {
|
||||
isEnabled:
|
||||
item.key === ROUTES.BILLING ||
|
||||
item.key === ROUTES.ROLES_SETTINGS ||
|
||||
item.key === ROUTES.ROLE_DETAILS ||
|
||||
item.key === ROUTES.INTEGRATIONS ||
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
@@ -140,8 +138,7 @@ function SettingsPage(): JSX.Element {
|
||||
isEnabled:
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.ROLES_SETTINGS ||
|
||||
item.key === ROUTES.ROLE_DETAILS
|
||||
item.key === ROUTES.ROLES_SETTINGS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
}));
|
||||
@@ -233,14 +230,6 @@ function SettingsPage(): JSX.Element {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
(pathname.startsWith(ROUTES.ROLES_SETTINGS) &&
|
||||
key === ROUTES.ROLES_SETTINGS) ||
|
||||
(pathname.startsWith(ROUTES.ROLE_DETAILS) && key === ROUTES.ROLE_DETAILS)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pathname === key;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSe
|
||||
import MySettings from 'container/MySettings';
|
||||
import OrganizationSettings from 'container/OrganizationSettings';
|
||||
import RolesSettings from 'container/RolesSettings';
|
||||
import RoleDetailsPage from 'container/RolesSettings/RoleDetails';
|
||||
import { TFunction } from 'i18next';
|
||||
import {
|
||||
Backpack,
|
||||
@@ -164,15 +163,6 @@ export const rolesSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
},
|
||||
];
|
||||
|
||||
export const roleDetails = (): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: RoleDetailsPage,
|
||||
name: <div className="periscope-tab">Role Details</div>,
|
||||
route: ROUTES.ROLE_DETAILS,
|
||||
key: ROUTES.ROLE_DETAILS,
|
||||
},
|
||||
];
|
||||
|
||||
export const keyboardShortcuts = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: Shortcuts,
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
multiIngestionSettings,
|
||||
mySettings,
|
||||
organizationSettings,
|
||||
roleDetails,
|
||||
rolesSettings,
|
||||
} from './config';
|
||||
|
||||
@@ -77,7 +76,6 @@ export const getRoutes = (
|
||||
...createAlertChannels(t),
|
||||
...editAlertChannels(t),
|
||||
...keyboardShortcuts(t),
|
||||
...roleDetails(),
|
||||
);
|
||||
|
||||
return settings;
|
||||
|
||||
@@ -98,7 +98,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
WORKSPACE_SUSPENDED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ROLES_SETTINGS: ['ADMIN'],
|
||||
ROLE_DETAILS: ['ADMIN'],
|
||||
BILLING: ['ADMIN'],
|
||||
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
||||
Reference in New Issue
Block a user