mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 22:29:02 +08:00
API ingestion keys - CRUD (#4524)
* feat: api keys crud - integration v0.1 * feat: add test cases * fix: add review comments * feat: api integration and ui updates * feat: update test cases * feat: update expiriesAt request payload * feat: ui feedback updates * feat: api keys crud - integration v0.1 * feat: add test cases * fix: add review comments * feat: api integration and ui updates * feat: update test cases * feat: update expiriesAt request payload * feat: ui feedback updates * feat: handle light mode styles * feat: hide pagination on single page * feat: do not show last used if not present or 0 * feat: show tooltip on role --------- Co-authored-by: Rajat Dabade <rajat@signoz.io>
This commit is contained in:
parent
c4bbbf372c
commit
e2669eb370
@ -20,6 +20,8 @@ const config: Config.InitialOptions = {
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||
'^.+\\.(css|scss|sass|less)$': 'jest-preview/transforms/css',
|
||||
'^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': 'jest-preview/transforms/file',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens)/)',
|
||||
|
@ -7,6 +7,7 @@
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-styled-components';
|
||||
import './src/styles.scss';
|
||||
|
||||
import { server } from './src/mocks-server/server';
|
||||
// Establish API mocking before all tests.
|
||||
|
@ -13,6 +13,8 @@
|
||||
"jest": "jest",
|
||||
"jest:coverage": "jest --coverage",
|
||||
"jest:watch": "jest --watch",
|
||||
"jest-preview": "jest-preview",
|
||||
"test:debug": "npm-run-all -p test jest-preview",
|
||||
"postinstall": "is-ci || yarn husky:configure",
|
||||
"playwright": "npm run i18n:generate-hash && NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
||||
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
|
||||
@ -77,7 +79,7 @@
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "0.288.0",
|
||||
"lucide-react": "0.321.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"papaparse": "5.4.1",
|
||||
"react": "18.2.0",
|
||||
@ -192,6 +194,7 @@
|
||||
"husky": "^7.0.4",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest-playwright-preset": "^1.7.2",
|
||||
"jest-preview": "0.3.1",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"lint-staged": "^12.5.0",
|
||||
"msw": "1.3.2",
|
||||
@ -208,7 +211,8 @@
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
"webpack-cli": "^4.9.2",
|
||||
"npm-run-all": "latest"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.(js|jsx|ts|tsx)": [
|
||||
|
@ -3,6 +3,7 @@
|
||||
"alert_channels": "Alert Channels",
|
||||
"organization_settings": "Organization Settings",
|
||||
"ingestion_settings": "Ingestion Settings",
|
||||
"api_keys": "API Keys",
|
||||
"my_settings": "My Settings",
|
||||
"overview_metrics": "Overview Metrics",
|
||||
"dbcall_metrics": "Database Calls",
|
||||
|
@ -26,6 +26,7 @@
|
||||
"MY_SETTINGS": "SigNoz | My Settings",
|
||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
|
||||
"API_KEYS": "SigNoz | API Keys",
|
||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
|
3
frontend/public/locales/en/apiKeys.json
Normal file
3
frontend/public/locales/en/apiKeys.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"delete_confirm_message": "Are you sure you want to delete {{keyName}} key? Deleting a key is irreversible and cannot be undone."
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
"alert_channels": "Alert Channels",
|
||||
"organization_settings": "Organization Settings",
|
||||
"ingestion_settings": "Ingestion Settings",
|
||||
"api_keys": "API Keys",
|
||||
"my_settings": "My Settings",
|
||||
"overview_metrics": "Overview Metrics",
|
||||
"dbcall_metrics": "Database Calls",
|
||||
|
@ -26,6 +26,7 @@
|
||||
"MY_SETTINGS": "SigNoz | My Settings",
|
||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
|
||||
"API_KEYS": "SigNoz | API Keys",
|
||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
|
@ -98,6 +98,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
|
||||
if (
|
||||
userResponse &&
|
||||
route &&
|
||||
route.find((e) => e === userResponse.payload.role) === undefined
|
||||
) {
|
||||
history.push(ROUTES.UN_AUTHORIZED);
|
||||
|
@ -118,6 +118,10 @@ export const IngestionSettings = Loadable(
|
||||
() => import(/* webpackChunkName: "Ingestion Settings" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const APIKeys = Loadable(
|
||||
() => import(/* webpackChunkName: "All Settings" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const MySettings = Loadable(
|
||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
||||
);
|
||||
|
@ -6,6 +6,7 @@ import { RouteProps } from 'react-router-dom';
|
||||
import {
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
APIKeys,
|
||||
BillingPage,
|
||||
CreateAlertChannelAlerts,
|
||||
CreateNewAlerts,
|
||||
@ -236,6 +237,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'INGESTION_SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.API_KEYS,
|
||||
exact: true,
|
||||
component: APIKeys,
|
||||
isPrivate: true,
|
||||
key: 'API_KEYS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.MY_SETTINGS,
|
||||
exact: true,
|
||||
|
26
frontend/src/api/APIKeys/createAPIKey.ts
Normal file
26
frontend/src/api/APIKeys/createAPIKey.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { APIKeyProps, CreateAPIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
const createAPIKey = async (
|
||||
props: CreateAPIKeyProps,
|
||||
): Promise<SuccessResponse<APIKeyProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/pats', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default createAPIKey;
|
24
frontend/src/api/APIKeys/deleteAPIKey.ts
Normal file
24
frontend/src/api/APIKeys/deleteAPIKey.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { AllAPIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
const deleteAPIKey = async (
|
||||
id: string,
|
||||
): Promise<SuccessResponse<AllAPIKeyProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/pats/${id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteAPIKey;
|
24
frontend/src/api/APIKeys/getAPIKey.ts
Normal file
24
frontend/src/api/APIKeys/getAPIKey.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/get';
|
||||
|
||||
const get = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/pats/${props.id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
6
frontend/src/api/APIKeys/getAllAPIKeys.ts
Normal file
6
frontend/src/api/APIKeys/getAllAPIKeys.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { AllAPIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
export const getAllAPIKeys = (): Promise<AxiosResponse<AllAPIKeyProps>> =>
|
||||
axios.get(`/pats`);
|
26
frontend/src/api/APIKeys/updateAPIKey.ts
Normal file
26
frontend/src/api/APIKeys/updateAPIKey.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, UpdateAPIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
const updateAPIKey = async (
|
||||
props: UpdateAPIKeyProps,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/pats/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default updateAPIKey;
|
@ -24,6 +24,7 @@ const ROUTES = {
|
||||
MY_SETTINGS: '/my-settings',
|
||||
SETTINGS: '/settings',
|
||||
ORG_SETTINGS: '/settings/org-settings',
|
||||
API_KEYS: '/settings/api-keys',
|
||||
INGESTION_SETTINGS: '/settings/ingestion-settings',
|
||||
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
||||
UN_AUTHORIZED: '/un-authorized',
|
||||
|
685
frontend/src/container/APIKeys/APIKeys.styles.scss
Normal file
685
frontend/src/container/APIKeys/APIKeys.styles.scss
Normal file
@ -0,0 +1,685 @@
|
||||
.api-key-container {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.api-key-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;
|
||||
}
|
||||
|
||||
.api-keys-search-add-new {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
padding: 16px 0;
|
||||
|
||||
.add-new-api-key-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
.ant-table-cell {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: var(--bg-ink-500);
|
||||
}
|
||||
.column-render {
|
||||
margin: 8px 0 !important;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
|
||||
.title-with-action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
|
||||
.api-key-data {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.api-key-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
border-radius: 20px;
|
||||
padding: 0px 12px;
|
||||
|
||||
background: var(--bg-ink-200);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.copy-key-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.visibility-btn {
|
||||
border: 1px solid rgba(113, 144, 249, 0.2);
|
||||
background: rgba(113, 144, 249, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse {
|
||||
border: none;
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0px 8px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #121317;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.ant-collapse-item {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-collapse-expand-icon {
|
||||
padding-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
border-top: 1px solid var(--bg-slate-500);
|
||||
padding: 8px;
|
||||
|
||||
.api-key-tag {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50px;
|
||||
background: var(--bg-slate-300);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-created-by {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.api-key-last-used-at {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.ant-typography {
|
||||
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;
|
||||
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||
slashed-zero;
|
||||
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-expires-in {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.dot {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--bg-amber-400);
|
||||
|
||||
.dot {
|
||||
background: var(--bg-amber-400);
|
||||
box-shadow: 0px 0px 6px 0px var(--bg-amber-400);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: var(--bg-cherry-400);
|
||||
|
||||
.dot {
|
||||
background: var(--bg-cherry-400);
|
||||
box-shadow: 0px 0px 6px 0px var(--bg-cherry-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-info-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-direction: column;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.user-avatar {
|
||||
background-color: lightslategray;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.user-email {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 20px;
|
||||
padding: 0px 12px;
|
||||
background: var(--bg-ink-200);
|
||||
|
||||
font-family: 'Space Mono', monospace;
|
||||
}
|
||||
|
||||
.role {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-modal {
|
||||
.ant-modal-content {
|
||||
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);
|
||||
padding: 0;
|
||||
|
||||
.ant-modal-header {
|
||||
background: none;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
padding: 16px;
|
||||
margin-top: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-access-role {
|
||||
display: flex;
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
font-size: 12px;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.ant-radio-button-wrapper-checked {
|
||||
color: #fff;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
border-color: var(--bg-slate-400, #1d212d);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
border-color: var(--bg-slate-400, #1d212d);
|
||||
|
||||
&::before {
|
||||
background-color: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
border-color: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
.role {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-api-key-modal {
|
||||
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.2);
|
||||
|
||||
.ant-modal-header {
|
||||
padding: 16px;
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0px 16px 28px 16px;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.api-key-input {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ant-color-picker-trigger {
|
||||
padding: 6px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
.ant-color-picker-color-block {
|
||||
border-radius: 50px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.ant-color-picker-color-block-inner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 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;
|
||||
}
|
||||
|
||||
.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% */
|
||||
}
|
||||
}
|
||||
|
||||
.expiration-selector {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.newAPIKeyDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.copyable-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 20px;
|
||||
padding: 0px 12px;
|
||||
background: var(--bg-ink-200, #23262e);
|
||||
|
||||
.copy-key-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.api-key-container {
|
||||
.api-key-content {
|
||||
.title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
.ant-table-cell {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-table-cell {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.column-render {
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-collapse {
|
||||
border: none;
|
||||
|
||||
.ant-collapse-header {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.title-with-action {
|
||||
.api-key-title {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-value {
|
||||
background: var(--bg-vanilla-200);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.copy-key-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-details {
|
||||
border-top: 1px solid var(--bg-vanilla-200);
|
||||
.api-key-tag {
|
||||
background: var(--bg-vanilla-200);
|
||||
.tag-text {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-created-by {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.api-key-last-used-at {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-api-key-modal {
|
||||
.ant-modal-content {
|
||||
border: 1px solid 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 {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.api-key-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-info-container {
|
||||
.user-email {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-modal {
|
||||
.ant-modal-content {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0;
|
||||
|
||||
.ant-modal-header {
|
||||
background: none;
|
||||
border-bottom: 1px solid var(--bg-vanilla-200);
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-access-role {
|
||||
.ant-radio-button-wrapper {
|
||||
&.ant-radio-button-wrapper-checked {
|
||||
color: var(--bg-ink-400);
|
||||
background: var(--bg-vanilla-300);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-400);
|
||||
background: var(--bg-vanilla-300);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&::before {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: var(--bg-ink-400);
|
||||
background: var(--bg-vanilla-300);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
&::before {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyable-text {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
99
frontend/src/container/APIKeys/APIKeys.test.tsx
Normal file
99
frontend/src/container/APIKeys/APIKeys.test.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import {
|
||||
createAPIKeyResponse,
|
||||
getAPIKeysResponse,
|
||||
} from 'mocks-server/__mockdata__/apiKeys';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import APIKeys from './APIKeys';
|
||||
|
||||
const apiKeysURL = 'http://localhost/api/v1/pats';
|
||||
|
||||
describe('APIKeys component', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(apiKeysURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(getAPIKeysResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<APIKeys />);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders APIKeys component without crashing', () => {
|
||||
expect(screen.getByText('API Keys')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Create and manage access keys for the SigNoz API'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render list of API Keys', async () => {
|
||||
server.use(
|
||||
rest.get(apiKeysURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(getAPIKeysResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No Expiry Token')).toBeInTheDocument();
|
||||
expect(screen.getByText('1-5 of 18 API Keys')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('opens add new key modal on button click', async () => {
|
||||
fireEvent.click(screen.getByText('New Key'));
|
||||
await waitFor(() => {
|
||||
const createNewKeyBtn = screen.getByRole('button', {
|
||||
name: /Create new key/i,
|
||||
});
|
||||
|
||||
expect(createNewKeyBtn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes add new key modal on cancel button click', async () => {
|
||||
fireEvent.click(screen.getByText('New Key'));
|
||||
|
||||
const createNewKeyBtn = screen.getByRole('button', {
|
||||
name: /Create new key/i,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createNewKeyBtn).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByText('Cancel'));
|
||||
await waitFor(() => {
|
||||
expect(createNewKeyBtn).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a new key on form submission', async () => {
|
||||
server.use(
|
||||
rest.post(apiKeysURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createAPIKeyResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('New Key'));
|
||||
|
||||
const createNewKeyBtn = screen.getByRole('button', {
|
||||
name: /Create new key/i,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createNewKeyBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
const inputElement = screen.getByPlaceholderText('Enter Key Name');
|
||||
fireEvent.change(inputElement, { target: { value: 'Top Secret' } });
|
||||
fireEvent.click(screen.getByTestId('create-form-admin-role-btn'));
|
||||
fireEvent.click(createNewKeyBtn);
|
||||
});
|
||||
});
|
||||
});
|
844
frontend/src/container/APIKeys/APIKeys.tsx
Normal file
844
frontend/src/container/APIKeys/APIKeys.tsx
Normal file
@ -0,0 +1,844 @@
|
||||
import './APIKeys.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Col,
|
||||
Collapse,
|
||||
Flex,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
Table,
|
||||
TableProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import { CollapseProps } from 'antd/lib';
|
||||
import createAPIKeyApi from 'api/APIKeys/createAPIKey';
|
||||
import deleteAPIKeyApi from 'api/APIKeys/deleteAPIKey';
|
||||
import updateAPIKeyApi from 'api/APIKeys/updateAPIKey';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { useGetAllAPIKeys } from 'hooks/APIKeys/useGetAllAPIKeys';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import {
|
||||
CalendarClock,
|
||||
Check,
|
||||
ClipboardEdit,
|
||||
Contact2,
|
||||
Copy,
|
||||
Eye,
|
||||
Minus,
|
||||
PenLine,
|
||||
Plus,
|
||||
Search,
|
||||
Trash2,
|
||||
View,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { APIKeyProps } from 'types/api/pat/types';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
export const showErrorNotification = (
|
||||
notifications: NotificationInstance,
|
||||
err: Error,
|
||||
): void => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
type ExpiryOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const EXPIRATION_WITHIN_SEVEN_DAYS = 7;
|
||||
|
||||
const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [
|
||||
{ value: '1', label: '1 day' },
|
||||
{ value: '7', label: '1 week' },
|
||||
{ value: '30', label: '1 month' },
|
||||
{ value: '90', label: '3 months' },
|
||||
{ value: '365', label: '1 year' },
|
||||
{ value: '0', label: 'No Expiry' },
|
||||
];
|
||||
|
||||
function APIKeys(): JSX.Element {
|
||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const { notifications } = useNotifications();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [showNewAPIKeyDetails, setShowNewAPIKeyDetails] = useState(false);
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [activeAPIKey, setActiveAPIKey] = useState<APIKeyProps | null>();
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [dataSource, setDataSource] = useState<APIKeyProps[]>([]);
|
||||
const { t } = useTranslation(['apiKeys']);
|
||||
|
||||
const [editForm] = Form.useForm();
|
||||
const [createForm] = Form.useForm();
|
||||
|
||||
const handleFormReset = (): void => {
|
||||
editForm.resetFields();
|
||||
createForm.resetFields();
|
||||
};
|
||||
|
||||
const hideDeleteViewModal = (): void => {
|
||||
handleFormReset();
|
||||
setActiveAPIKey(null);
|
||||
setIsDeleteModalOpen(false);
|
||||
};
|
||||
|
||||
const showDeleteModal = (apiKey: APIKeyProps): void => {
|
||||
setActiveAPIKey(apiKey);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const hideEditViewModal = (): void => {
|
||||
handleFormReset();
|
||||
setActiveAPIKey(null);
|
||||
setIsEditModalOpen(false);
|
||||
};
|
||||
|
||||
const hideAddViewModal = (): void => {
|
||||
handleFormReset();
|
||||
setShowNewAPIKeyDetails(false);
|
||||
setActiveAPIKey(null);
|
||||
setIsAddModalOpen(false);
|
||||
};
|
||||
|
||||
const showEditModal = (apiKey: APIKeyProps): void => {
|
||||
handleFormReset();
|
||||
setActiveAPIKey(apiKey);
|
||||
|
||||
editForm.setFieldsValue({
|
||||
name: apiKey.name,
|
||||
role: apiKey.role || USER_ROLES.VIEWER,
|
||||
});
|
||||
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const showAddModal = (): void => {
|
||||
setActiveAPIKey(null);
|
||||
setIsAddModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModalClose = (): void => {
|
||||
setActiveAPIKey(null);
|
||||
};
|
||||
|
||||
const {
|
||||
data: APIKeys,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
refetch: refetchAPIKeys,
|
||||
error,
|
||||
isError,
|
||||
} = useGetAllAPIKeys();
|
||||
|
||||
useEffect(() => {
|
||||
setActiveAPIKey(APIKeys?.data.data[0]);
|
||||
}, [APIKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(APIKeys?.data.data || []);
|
||||
}, [APIKeys?.data.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
}
|
||||
}, [error, isError, notifications]);
|
||||
|
||||
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setSearchValue(e.target.value);
|
||||
const filteredData = APIKeys?.data?.data?.filter(
|
||||
(key: APIKeyProps) =>
|
||||
key &&
|
||||
key.name &&
|
||||
key.name.toLowerCase().includes(e.target.value.toLowerCase()),
|
||||
);
|
||||
setDataSource(filteredData || []);
|
||||
};
|
||||
|
||||
const clearSearch = (): void => {
|
||||
setSearchValue('');
|
||||
};
|
||||
|
||||
const { mutate: createAPIKey, isLoading: isLoadingCreateAPIKey } = useMutation(
|
||||
createAPIKeyApi,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setShowNewAPIKeyDetails(true);
|
||||
setActiveAPIKey(data.payload);
|
||||
|
||||
refetchAPIKeys();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation(
|
||||
updateAPIKeyApi,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchAPIKeys();
|
||||
setIsEditModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation(
|
||||
deleteAPIKeyApi,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchAPIKeys();
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onDeleteHandler = (): void => {
|
||||
clearSearch();
|
||||
|
||||
if (activeAPIKey) {
|
||||
deleteAPIKey(activeAPIKey.id);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateApiKey = (): void => {
|
||||
editForm
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
if (activeAPIKey) {
|
||||
updateAPIKey({
|
||||
id: activeAPIKey.id,
|
||||
data: {
|
||||
name: values.name,
|
||||
role: values.role,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.error('error info', errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateAPIKey = (): void => {
|
||||
createForm
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
if (user) {
|
||||
createAPIKey({
|
||||
name: values.name,
|
||||
expiresInDays: parseInt(values.expiration, 10),
|
||||
role: values.role,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.error('error info', errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyKey = (text: string): void => {
|
||||
handleCopyToClipboard(text);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
const getFormattedTime = (epochTime: number): string => {
|
||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
const formattedTime = new Date(epochTime * 1000).toLocaleTimeString(
|
||||
'en-US',
|
||||
timeOptions,
|
||||
);
|
||||
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
const formattedDate = new Date(epochTime * 1000).toLocaleDateString(
|
||||
'en-US',
|
||||
dateOptions,
|
||||
);
|
||||
|
||||
return `${formattedDate} ${formattedTime}`;
|
||||
};
|
||||
|
||||
const handleCopyClose = (): void => {
|
||||
if (activeAPIKey) {
|
||||
handleCopyKey(activeAPIKey?.token);
|
||||
}
|
||||
|
||||
hideAddViewModal();
|
||||
};
|
||||
|
||||
const getDateDifference = (
|
||||
createdTimestamp: number,
|
||||
expiryTimestamp: number,
|
||||
): number => {
|
||||
const differenceInSeconds = Math.abs(expiryTimestamp - createdTimestamp);
|
||||
|
||||
// Convert seconds to days
|
||||
return differenceInSeconds / (60 * 60 * 24);
|
||||
};
|
||||
|
||||
const columns: TableProps<APIKeyProps>['columns'] = [
|
||||
{
|
||||
title: 'API Key',
|
||||
key: 'api-key',
|
||||
render: (APIKey: APIKeyProps): JSX.Element => {
|
||||
const formattedDateAndTime =
|
||||
APIKey && APIKey?.lastUsed && APIKey?.lastUsed !== 0
|
||||
? getFormattedTime(APIKey?.lastUsed)
|
||||
: 'Never';
|
||||
|
||||
const createdOn = getFormattedTime(APIKey.createdAt);
|
||||
|
||||
const expiresIn =
|
||||
APIKey.expiresAt === 0
|
||||
? Number.POSITIVE_INFINITY
|
||||
: getDateDifference(APIKey?.createdAt, APIKey?.expiresAt);
|
||||
|
||||
const expiresOn =
|
||||
!APIKey.expiresAt || APIKey.expiresAt === 0
|
||||
? 'No Expiry'
|
||||
: getFormattedTime(APIKey.expiresAt);
|
||||
|
||||
const updatedOn =
|
||||
!APIKey.updatedAt || APIKey.updatedAt === 0
|
||||
? null
|
||||
: getFormattedTime(APIKey?.updatedAt);
|
||||
|
||||
const items: CollapseProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="title-with-action">
|
||||
<div className="api-key-data">
|
||||
<div className="api-key-title">
|
||||
<Typography.Text>{APIKey?.name}</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="api-key-value">
|
||||
<Typography.Text>
|
||||
{APIKey?.token.substring(0, 2)}********
|
||||
{APIKey?.token.substring(APIKey.token.length - 2).trim()}
|
||||
</Typography.Text>
|
||||
|
||||
<Copy
|
||||
className="copy-key-btn"
|
||||
size={12}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCopyKey(APIKey.token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{APIKey.role === USER_ROLES.ADMIN && (
|
||||
<Tooltip title={USER_ROLES.ADMIN}>
|
||||
<Contact2 size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{APIKey.role === USER_ROLES.EDITOR && (
|
||||
<Tooltip title={USER_ROLES.EDITOR}>
|
||||
<ClipboardEdit size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{APIKey.role === USER_ROLES.VIEWER && (
|
||||
<Tooltip title={USER_ROLES.VIEWER}>
|
||||
<View size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!APIKey.role && (
|
||||
<Tooltip title={USER_ROLES.ADMIN}>
|
||||
<Contact2 size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="action-btn">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showEditModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<div className="api-key-info-container">
|
||||
{APIKey?.createdByUser && (
|
||||
<Row>
|
||||
<Col span={6}> Creator </Col>
|
||||
<Col span={12} className="user-info">
|
||||
<Avatar className="user-avatar" size="small">
|
||||
{APIKey?.createdByUser?.name?.substring(0, 1)}
|
||||
</Avatar>
|
||||
|
||||
<Typography.Text>{APIKey.createdByUser?.name}</Typography.Text>
|
||||
|
||||
<div className="user-email">{APIKey.createdByUser?.email}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Col span={6}> Created on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{createdOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
{updatedOn && (
|
||||
<Row>
|
||||
<Col span={6}> Updated on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{updatedOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row>
|
||||
<Col span={6}> Expires on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{expiresOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="column-render">
|
||||
<Collapse items={items} />
|
||||
|
||||
<div className="api-key-details">
|
||||
<div className="api-key-last-used-at">
|
||||
<CalendarClock size={14} />
|
||||
Last used <Minus size={12} />
|
||||
<Typography.Text>{formattedDateAndTime}</Typography.Text>
|
||||
</div>
|
||||
{expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && (
|
||||
<div
|
||||
className={cx(
|
||||
'api-key-expires-in',
|
||||
expiresIn <= 3 ? 'danger' : 'warning',
|
||||
)}
|
||||
>
|
||||
<span className="dot" /> Expires in {expiresIn} Days
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="api-key-container">
|
||||
<div className="api-key-content">
|
||||
<header>
|
||||
<Typography.Title className="title">API Keys</Typography.Title>
|
||||
<Typography.Text className="subtitle">
|
||||
Create and manage access keys for the SigNoz API
|
||||
</Typography.Text>
|
||||
</header>
|
||||
|
||||
<div className="api-keys-search-add-new">
|
||||
<Input
|
||||
placeholder="Search for keys..."
|
||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||
value={searchValue}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="add-new-api-key-btn"
|
||||
type="primary"
|
||||
onClick={showAddModal}
|
||||
>
|
||||
<Plus size={14} /> New Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={isLoading || isRefetching}
|
||||
showHeader={false}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
hideOnSinglePage: true,
|
||||
showTotal: (total: number, range: number[]): string =>
|
||||
`${range[0]}-${range[1]} of ${total} API Keys`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Delete Key Modal */}
|
||||
<Modal
|
||||
className="delete-api-key-modal"
|
||||
title={<span className="title">Delete key</span>}
|
||||
open={isDeleteModalOpen}
|
||||
closable
|
||||
afterClose={handleModalClose}
|
||||
onCancel={hideDeleteViewModal}
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideDeleteViewModal}
|
||||
className="cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
icon={<Trash2 size={16} />}
|
||||
loading={isDeleteingAPIKey}
|
||||
onClick={onDeleteHandler}
|
||||
className="delete-btn"
|
||||
>
|
||||
Delete key
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Typography.Text className="delete-text">
|
||||
{t('delete_confirm_message', {
|
||||
keyName: activeAPIKey?.name,
|
||||
})}
|
||||
</Typography.Text>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Key Modal */}
|
||||
<Modal
|
||||
className="api-key-modal"
|
||||
title="Edit key"
|
||||
open={isEditModalOpen}
|
||||
key="edit-api-key-modal"
|
||||
afterClose={handleModalClose}
|
||||
// closable
|
||||
onCancel={hideEditViewModal}
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideEditViewModal}
|
||||
className="periscope-btn cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
className="periscope-btn primary"
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={isLoadingUpdateAPIKey}
|
||||
icon={<Check size={14} />}
|
||||
onClick={onUpdateApiKey}
|
||||
>
|
||||
Update key
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
name="edit-api-key-form"
|
||||
key={activeAPIKey?.id}
|
||||
form={editForm}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
initialValues={{
|
||||
name: activeAPIKey?.name,
|
||||
role: activeAPIKey?.role,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
||||
>
|
||||
<Input placeholder="Enter Key Name" autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="role" label="Role">
|
||||
<Flex vertical gap="middle">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
className="api-key-access-role"
|
||||
defaultValue={activeAPIKey?.role}
|
||||
>
|
||||
<Radio.Button value={USER_ROLES.ADMIN} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.EDITOR} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.VIEWER} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<Eye size={14} /> Viewer
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Create New Key Modal */}
|
||||
<Modal
|
||||
className="api-key-modal"
|
||||
title="Create new key"
|
||||
open={isAddModalOpen}
|
||||
key="create-api-key-modal"
|
||||
closable
|
||||
onCancel={hideAddViewModal}
|
||||
destroyOnClose
|
||||
footer={
|
||||
showNewAPIKeyDetails
|
||||
? [
|
||||
<Button
|
||||
key="copy-key-close"
|
||||
className="periscope-btn primary"
|
||||
data-testid="copy-key-close-btn"
|
||||
type="primary"
|
||||
onClick={handleCopyClose}
|
||||
icon={<Check size={12} />}
|
||||
>
|
||||
Copy key and close
|
||||
</Button>,
|
||||
]
|
||||
: [
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideAddViewModal}
|
||||
className="periscope-btn cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
className="periscope-btn primary"
|
||||
test-id="create-new-key"
|
||||
key="submit"
|
||||
type="primary"
|
||||
icon={<Check size={14} />}
|
||||
loading={isLoadingCreateAPIKey}
|
||||
onClick={onCreateAPIKey}
|
||||
>
|
||||
Create new key
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
>
|
||||
{!showNewAPIKeyDetails && (
|
||||
<Form
|
||||
key="createForm"
|
||||
name="create-api-key-form"
|
||||
form={createForm}
|
||||
initialValues={{
|
||||
role: USER_ROLES.ADMIN,
|
||||
expiration: '1',
|
||||
name: '',
|
||||
}}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
||||
validateTrigger="onFinish"
|
||||
>
|
||||
<Input placeholder="Enter Key Name" autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="role" label="Role">
|
||||
<Flex vertical gap="middle">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
className="api-key-access-role"
|
||||
defaultValue={USER_ROLES.ADMIN}
|
||||
>
|
||||
<Radio.Button value={USER_ROLES.ADMIN} className={cx('tab')}>
|
||||
<div className="role" data-testid="create-form-admin-role-btn">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.EDITOR} className="tab">
|
||||
<div className="role" data-testid="create-form-editor-role-btn">
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.VIEWER} className="tab">
|
||||
<div className="role" data-testid="create-form-viewer-role-btn">
|
||||
<Eye size={14} /> Viewer
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item name="expiration" label="Expiration">
|
||||
<Select
|
||||
className="expiration-selector"
|
||||
placeholder="Expiration"
|
||||
options={API_KEY_EXPIRY_OPTIONS}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{showNewAPIKeyDetails && (
|
||||
<div className="api-key-info-container">
|
||||
<Row>
|
||||
<Col span={8}>Key</Col>
|
||||
<Col span={16}>
|
||||
<span className="copyable-text">
|
||||
<Typography.Text>
|
||||
{activeAPIKey?.token.substring(0, 2)}****************
|
||||
{activeAPIKey?.token.substring(activeAPIKey.token.length - 2).trim()}
|
||||
</Typography.Text>
|
||||
|
||||
<Copy
|
||||
className="copy-key-btn"
|
||||
size={12}
|
||||
onClick={(): void => {
|
||||
if (activeAPIKey) {
|
||||
handleCopyKey(activeAPIKey.token);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Name</Col>
|
||||
<Col span={16}>{activeAPIKey?.name}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Role</Col>
|
||||
<Col span={16}>
|
||||
{activeAPIKey?.role === USER_ROLES.ADMIN && (
|
||||
<div className="role">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
)}
|
||||
{activeAPIKey?.role === USER_ROLES.EDITOR && (
|
||||
<div className="role">
|
||||
{' '}
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
)}
|
||||
{activeAPIKey?.role === USER_ROLES.VIEWER && (
|
||||
<div className="role">
|
||||
{' '}
|
||||
<View size={14} /> Viewer
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Creator</Col>
|
||||
|
||||
<Col span={16} className="user-info">
|
||||
<Avatar className="user-avatar" size="small">
|
||||
{activeAPIKey?.createdByUser?.name?.substring(0, 1)}
|
||||
</Avatar>
|
||||
|
||||
<Typography.Text>{activeAPIKey?.createdByUser?.name}</Typography.Text>
|
||||
|
||||
<div className="user-email">{activeAPIKey?.createdByUser?.email}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{activeAPIKey?.createdAt && (
|
||||
<Row>
|
||||
<Col span={8}>Created on</Col>
|
||||
<Col span={16}>{getFormattedTime(activeAPIKey?.createdAt)}</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.expiresAt !== 0 && activeAPIKey?.expiresAt && (
|
||||
<Row>
|
||||
<Col span={8}>Expires on</Col>
|
||||
<Col span={16}>{getFormattedTime(activeAPIKey?.expiresAt)}</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.expiresAt === 0 && (
|
||||
<Row>
|
||||
<Col span={8}>Expires on</Col>
|
||||
<Col span={16}> No Expiry </Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default APIKeys;
|
@ -121,6 +121,7 @@ export const routesToSkip = [
|
||||
ROUTES.ALL_DASHBOARD,
|
||||
ROUTES.ORG_SETTINGS,
|
||||
ROUTES.INGESTION_SETTINGS,
|
||||
ROUTES.API_KEYS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.LOGS_PIPELINES,
|
||||
ROUTES.BILLING,
|
||||
|
13
frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts
Normal file
13
frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { getAllAPIKeys } from 'api/APIKeys/getAllAPIKeys';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { AllAPIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
export const useGetAllAPIKeys = (): UseQueryResult<
|
||||
AxiosResponse<AllAPIKeyProps>,
|
||||
AxiosError
|
||||
> =>
|
||||
useQuery<AxiosResponse<AllAPIKeyProps>, AxiosError>({
|
||||
queryKey: ['APIKeys'],
|
||||
queryFn: () => getAllAPIKeys(),
|
||||
});
|
541
frontend/src/mocks-server/__mockdata__/apiKeys.ts
Normal file
541
frontend/src/mocks-server/__mockdata__/apiKeys.ts
Normal file
@ -0,0 +1,541 @@
|
||||
const createdByEmail = 'mando@signoz.io';
|
||||
|
||||
export const getAPIKeysResponse = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
id: '26',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'T2DuASwpuUx3wlYraFl5r7N9G1ikBhzGuy2ihcIKDMs=',
|
||||
role: 'ADMIN',
|
||||
name: '1 Day Old',
|
||||
createdAt: 1708010258,
|
||||
expiresAt: 1708096658,
|
||||
updatedAt: 1708010258,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '24',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'EteVs77BA4FFLJD/TsFE9c+CLX4kXVmlx+0GGK7dpXY=',
|
||||
role: 'ADMIN',
|
||||
name: '1 year expiry - updated',
|
||||
createdAt: 1708008146,
|
||||
expiresAt: 1739544146,
|
||||
updatedAt: 1708008239,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '25',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: '1udrUFmRI6gdb8r/hLabS7zRlgfMQlUw/tz9sac82pE=',
|
||||
role: 'ADMIN',
|
||||
name: 'No Expiry Token',
|
||||
createdAt: 1708008178,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1708008190,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'gtqKF7g7avoe+Yu2+WhyDDLQSr6IsVaR5xpby2XhLAY=',
|
||||
role: 'VIEWER',
|
||||
name: 'No Expiry',
|
||||
createdAt: 1708007395,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1708007936,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'GM/TqEID8N4ynlvQHK38ITEvRAcn5XkJZpmd11xT3OQ=',
|
||||
role: 'VIEWER',
|
||||
name: 'No Expiry - 2',
|
||||
createdAt: 1708007685,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1708007786,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'Oj75e6Zr7JmjFcWIo0UK/Nl06RdC2BKOr/QVHoBA0gM=',
|
||||
role: 'ADMIN',
|
||||
name: '7 Days',
|
||||
createdAt: 1708003326,
|
||||
expiresAt: 1708608126,
|
||||
updatedAt: 1708007380,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'T+sNdYe6I74ya/9mEKqB3UTrFm8+jwI0DiirqEx3bsM=',
|
||||
role: 'EDITOR',
|
||||
name: '1 month',
|
||||
createdAt: 1708004012,
|
||||
expiresAt: 1710596012,
|
||||
updatedAt: 1708005206,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'JWw26FuymeHq+fsfFcb+2+Ls/MdokmeXxXdZisuaVeI=',
|
||||
role: 'ADMIN',
|
||||
name: '3 Months',
|
||||
createdAt: 1708004755,
|
||||
expiresAt: 1715780755,
|
||||
updatedAt: 1708005197,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: '2zDrYNr+IWXUyA14+afVvO6GI9dcHfEsOYxjA9mrprg=',
|
||||
role: 'ADMIN',
|
||||
name: 'New No Expiry',
|
||||
createdAt: 1708000444,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1708000444,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'Q+/+UB2OrDPcS9b0+5A1dDXYmWHz0abbVVidF48QCso=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user 1',
|
||||
createdAt: 1707997720,
|
||||
expiresAt: 1708170520,
|
||||
updatedAt: 1707997720,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: '/X3OEaSOLrrJImvzIB3g5WGg+5831X89fZZQT1JaxvQ=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user 2',
|
||||
createdAt: 1707997603,
|
||||
expiresAt: 1708170403,
|
||||
updatedAt: 1707997603,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'bTs+Q6waIiP4KJ8L5N58EQonuapWMXsfEra/cmMwmbE=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user 3',
|
||||
createdAt: 1707997539,
|
||||
expiresAt: 1708170339,
|
||||
updatedAt: 1707997539,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'YaEqQHrH8KOnYFllor/8Tq653TgxPU1Z7ZDzY3+ETmI=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user',
|
||||
createdAt: 1707997537,
|
||||
expiresAt: 1708170337,
|
||||
updatedAt: 1707997537,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'Hg/QpMU9VQyqIuzSh9ND2454IN5uOHzVkv7owEtBcPo=',
|
||||
role: 'EDITOR',
|
||||
name: 'test123',
|
||||
createdAt: 1707997288,
|
||||
expiresAt: 1708083688,
|
||||
updatedAt: 1707997288,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'M5gMsccDthPTibquB7kR7ZSEI76y4endOxZPESZ9/po=',
|
||||
role: 'VIEWER',
|
||||
name: 'Viewer Token for user',
|
||||
createdAt: 1707996747,
|
||||
expiresAt: 1708255947,
|
||||
updatedAt: 1707996747,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'H8NVlOD09IcMgQ/rzfVucb+4+jEcqZ4ZRx6n7QztMSc=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user',
|
||||
createdAt: 1707996736,
|
||||
expiresAt: 1708169536,
|
||||
updatedAt: 1707996736,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'z24SswLmNlPVUgb1j6rfc2u4Kb4xSUolwb11cI8kbrs=',
|
||||
role: 'ADMIN',
|
||||
name: 'Admin Token for user',
|
||||
createdAt: 1707996719,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1707996719,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'SWuNSF08EB6+VN05312QaAsPum2wkqIm+ujiWZKnm2Q=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token',
|
||||
createdAt: 1707992270,
|
||||
expiresAt: 1708165070,
|
||||
updatedAt: 1707995424,
|
||||
lastUsed: 1707992517,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const createAPIKeyResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
id: '57',
|
||||
userId: 'mandalorian',
|
||||
token: 'pQ5kiHjcbQ2FbKlS14LQjA2RzXEBi/KvBfM7BRSwltI=',
|
||||
name: 'test1233',
|
||||
createdAt: 1707818550,
|
||||
expiresAt: 0,
|
||||
},
|
||||
};
|
@ -1,16 +1,22 @@
|
||||
import { RouteTabProps } from 'components/RouteTab/types';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import APIKeys from 'container/APIKeys/APIKeys';
|
||||
import GeneralSettings from 'container/GeneralSettings';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
|
||||
import OrganizationSettings from 'container/OrganizationSettings';
|
||||
import { TFunction } from 'i18next';
|
||||
import { Backpack, BellDot, Building, Cpu, KeySquare } from 'lucide-react';
|
||||
|
||||
export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: OrganizationSettings,
|
||||
name: t('routes:organization_settings').toString(),
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<Building size={16} /> {t('routes:organization_settings').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.ORG_SETTINGS,
|
||||
key: ROUTES.ORG_SETTINGS,
|
||||
},
|
||||
@ -19,7 +25,11 @@ export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
export const alertChannels = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: AlertChannels,
|
||||
name: t('routes:alert_channels').toString(),
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<BellDot size={16} /> {t('routes:alert_channels').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.ALL_CHANNELS,
|
||||
key: ROUTES.ALL_CHANNELS,
|
||||
},
|
||||
@ -28,7 +38,11 @@ export const alertChannels = (t: TFunction): RouteTabProps['routes'] => [
|
||||
export const ingestionSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: IngestionSettings,
|
||||
name: t('routes:ingestion_settings').toString(),
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<Cpu size={16} /> {t('routes:ingestion_settings').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.INGESTION_SETTINGS,
|
||||
key: ROUTES.INGESTION_SETTINGS,
|
||||
},
|
||||
@ -37,7 +51,11 @@ export const ingestionSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
export const generalSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: GeneralSettings,
|
||||
name: t('routes:general').toString(),
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<Backpack size={16} /> {t('routes:general').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.SETTINGS,
|
||||
key: ROUTES.SETTINGS,
|
||||
},
|
||||
@ -46,8 +64,25 @@ export const generalSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
export const generalSettingsCloud = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: GeneralSettingsCloud,
|
||||
name: t('routes:general').toString(),
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<Backpack size={16} /> {t('routes:general').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.SETTINGS,
|
||||
key: ROUTES.SETTINGS,
|
||||
},
|
||||
];
|
||||
|
||||
export const apiKeys = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: APIKeys,
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<KeySquare size={16} /> {t('routes:api_keys').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.API_KEYS,
|
||||
key: ROUTES.API_KEYS,
|
||||
},
|
||||
];
|
@ -19,7 +19,8 @@ function SettingsPage(): JSX.Element {
|
||||
);
|
||||
const { t } = useTranslation(['routes']);
|
||||
|
||||
const routes = useMemo(() => getRoutes(isCurrentOrgSettings, t), [
|
||||
const routes = useMemo(() => getRoutes(role, isCurrentOrgSettings, t), [
|
||||
role,
|
||||
isCurrentOrgSettings,
|
||||
t,
|
||||
]);
|
||||
|
@ -1,20 +1,25 @@
|
||||
import { RouteTabProps } from 'components/RouteTab/types';
|
||||
import { TFunction } from 'i18next';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { isCloudUser, isEECloudUser } from 'utils/app';
|
||||
|
||||
import {
|
||||
alertChannels,
|
||||
apiKeys,
|
||||
generalSettings,
|
||||
ingestionSettings,
|
||||
organizationSettings,
|
||||
} from './config';
|
||||
|
||||
export const getRoutes = (
|
||||
userRole: ROLES | null,
|
||||
isCurrentOrgSettings: boolean,
|
||||
t: TFunction,
|
||||
): RouteTabProps['routes'] => {
|
||||
const settings = [];
|
||||
|
||||
settings.push(...generalSettings(t));
|
||||
|
||||
if (isCurrentOrgSettings) {
|
||||
settings.push(...organizationSettings(t));
|
||||
}
|
||||
@ -26,7 +31,9 @@ export const getRoutes = (
|
||||
settings.push(...alertChannels(t));
|
||||
}
|
||||
|
||||
settings.push(...generalSettings(t));
|
||||
if ((isCloudUser() || isEECloudUser()) && userRole === USER_ROLES.ADMIN) {
|
||||
settings.push(...apiKeys(t));
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
@ -24,6 +24,18 @@
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.primary {
|
||||
color: #fff;
|
||||
background-color: #4566d6;
|
||||
box-shadow: 0 2px 0 rgba(62, 86, 245, 0.09);
|
||||
}
|
||||
}
|
||||
|
||||
.periscope-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
|
53
frontend/src/types/api/pat/types.ts
Normal file
53
frontend/src/types/api/pat/types.ts
Normal file
@ -0,0 +1,53 @@
|
||||
export interface User {
|
||||
createdAt?: number;
|
||||
email?: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
notFound?: boolean;
|
||||
profilePictureURL?: string;
|
||||
}
|
||||
|
||||
export interface APIKeyProps {
|
||||
name: string;
|
||||
expiresAt: number;
|
||||
role: string;
|
||||
token: string;
|
||||
id: string;
|
||||
createdAt: number;
|
||||
createdByUser?: User;
|
||||
updatedAt?: number;
|
||||
updatedByUser?: User;
|
||||
lastUsed?: number;
|
||||
}
|
||||
|
||||
export interface CreateAPIKeyProps {
|
||||
name: string;
|
||||
expiresInDays: number;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface AllAPIKeyProps {
|
||||
status: string;
|
||||
data: APIKeyProps[];
|
||||
}
|
||||
|
||||
export interface CreateAPIKeyProp {
|
||||
data: APIKeyProps;
|
||||
}
|
||||
|
||||
export interface DeleteAPIKeyPayloadProps {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface UpdateAPIKeyProps {
|
||||
id: string;
|
||||
data: {
|
||||
name: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
status: string;
|
||||
data: string;
|
||||
};
|
@ -88,6 +88,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGS_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
API_KEYS: ['ADMIN'],
|
||||
LOGS_BASE: [],
|
||||
OLD_LOGS_EXPLORER: [],
|
||||
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
@ -23,6 +23,11 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@constants/*": [
|
||||
"/container/OnboardingContainer/constants/*"
|
||||
]
|
||||
},
|
||||
"downlevelIteration": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user