mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 06:49:01 +08:00
feat: dashboard perf improvements (#4010)
* feat: dashboard perf improvements * feat: remove console logs * fix: remove console.log * fix: update tests * fix: update tests --------- Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
parent
07d126c669
commit
b2d6d75eef
@ -49,7 +49,8 @@ export const Onboarding = Loadable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const DashboardPage = Loadable(
|
export const DashboardPage = Loadable(
|
||||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/Dashboard'),
|
() =>
|
||||||
|
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const NewDashboardPage = Loadable(
|
export const NewDashboardPage = Loadable(
|
||||||
|
@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
|
|||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
||||||
|
|
||||||
const update = async (
|
const updateDashboard = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
@ -23,4 +23,4 @@ const update = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default update;
|
export default updateDashboard;
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
Props,
|
||||||
|
VariableResponseProps,
|
||||||
|
} from 'types/api/dashboard/variables/query';
|
||||||
|
|
||||||
|
const dashboardVariablesQuery = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/variables/query`, props);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const formattedError = ErrorResponseHandler(error as AxiosError);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
|
throw { message: 'Error fetching data', details: formattedError };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dashboardVariablesQuery;
|
@ -1,24 +0,0 @@
|
|||||||
import { ApiV2Instance as axios } from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/dashboard/variables/query';
|
|
||||||
|
|
||||||
const query = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`/variables/query`, props);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default query;
|
|
378
frontend/src/container/ListOfDashboard/DashboardsList.tsx
Normal file
378
frontend/src/container/ListOfDashboard/DashboardsList.tsx
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
|
||||||
|
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
||||||
|
import createDashboard from 'api/dashboard/create';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import {
|
||||||
|
DynamicColumnsKey,
|
||||||
|
TableDataSource,
|
||||||
|
} from 'components/ResizeTable/contants';
|
||||||
|
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
||||||
|
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
||||||
|
import TextToolTip from 'components/TextToolTip';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||||
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { generatePath } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
|
||||||
|
import ImportJSON from './ImportJSON';
|
||||||
|
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
|
||||||
|
import DeleteButton from './TableComponents/DeleteButton';
|
||||||
|
import Name from './TableComponents/Name';
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
function DashboardsList(): JSX.Element {
|
||||||
|
const {
|
||||||
|
data: dashboardListResponse = [],
|
||||||
|
isLoading: isDashboardListLoading,
|
||||||
|
refetch: refetchDashboardList,
|
||||||
|
} = useGetAllDashboard();
|
||||||
|
|
||||||
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const [action, createNewDashboard] = useComponentPermission(
|
||||||
|
['action', 'create_new_dashboards'],
|
||||||
|
role,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation('dashboard');
|
||||||
|
|
||||||
|
const [
|
||||||
|
isImportJSONModalVisible,
|
||||||
|
setIsImportJSONModalVisible,
|
||||||
|
] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
||||||
|
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
|
||||||
|
|
||||||
|
const [dashboards, setDashboards] = useState<Dashboard[]>();
|
||||||
|
|
||||||
|
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
||||||
|
const sortedDashboards = dashboards.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||||
|
);
|
||||||
|
setDashboards(sortedDashboards);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sortDashboardsByCreatedAt(dashboardListResponse);
|
||||||
|
}, [dashboardListResponse]);
|
||||||
|
|
||||||
|
const [newDashboardState, setNewDashboardState] = useState({
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicColumns: TableColumnProps<Data>[] = [
|
||||||
|
{
|
||||||
|
title: 'Created At',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
width: 30,
|
||||||
|
key: DynamicColumnsKey.CreatedAt,
|
||||||
|
sorter: (a: Data, b: Data): number => {
|
||||||
|
console.log({ a });
|
||||||
|
const prev = new Date(a.createdAt).getTime();
|
||||||
|
const next = new Date(b.createdAt).getTime();
|
||||||
|
|
||||||
|
return prev - next;
|
||||||
|
},
|
||||||
|
render: DateComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Created By',
|
||||||
|
dataIndex: 'createdBy',
|
||||||
|
width: 30,
|
||||||
|
key: DynamicColumnsKey.CreatedBy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Last Updated Time',
|
||||||
|
width: 30,
|
||||||
|
dataIndex: 'lastUpdatedTime',
|
||||||
|
key: DynamicColumnsKey.UpdatedAt,
|
||||||
|
sorter: (a: Data, b: Data): number => {
|
||||||
|
const prev = new Date(a.lastUpdatedTime).getTime();
|
||||||
|
const next = new Date(b.lastUpdatedTime).getTime();
|
||||||
|
|
||||||
|
return prev - next;
|
||||||
|
},
|
||||||
|
render: DateComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Last Updated By',
|
||||||
|
dataIndex: 'lastUpdatedBy',
|
||||||
|
width: 30,
|
||||||
|
key: DynamicColumnsKey.UpdatedBy,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
const tableColumns: TableColumnProps<Data>[] = [
|
||||||
|
{
|
||||||
|
title: 'Name',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 40,
|
||||||
|
render: Name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Description',
|
||||||
|
width: 50,
|
||||||
|
dataIndex: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tags',
|
||||||
|
dataIndex: 'tags',
|
||||||
|
width: 50,
|
||||||
|
render: (value): JSX.Element => <LabelColumn labels={value} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
tableColumns.push({
|
||||||
|
title: 'Action',
|
||||||
|
dataIndex: '',
|
||||||
|
width: 40,
|
||||||
|
render: DeleteButton,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableColumns;
|
||||||
|
}, [action]);
|
||||||
|
|
||||||
|
const data: Data[] =
|
||||||
|
dashboards?.map((e) => ({
|
||||||
|
createdAt: e.created_at,
|
||||||
|
description: e.data.description || '',
|
||||||
|
id: e.uuid,
|
||||||
|
lastUpdatedTime: e.updated_at,
|
||||||
|
name: e.data.title,
|
||||||
|
tags: e.data.tags || [],
|
||||||
|
key: e.uuid,
|
||||||
|
createdBy: e.created_by,
|
||||||
|
isLocked: !!e.isLocked || false,
|
||||||
|
lastUpdatedBy: e.updated_by,
|
||||||
|
refetchDashboardList,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const onNewDashboardHandler = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setNewDashboardState({
|
||||||
|
...newDashboardState,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
const response = await createDashboard({
|
||||||
|
title: t('new_dashboard_title', {
|
||||||
|
ns: 'dashboard',
|
||||||
|
}),
|
||||||
|
uploadedGrafana: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
history.push(
|
||||||
|
generatePath(ROUTES.DASHBOARD, {
|
||||||
|
dashboardId: response.payload.uuid,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setNewDashboardState({
|
||||||
|
...newDashboardState,
|
||||||
|
loading: false,
|
||||||
|
error: true,
|
||||||
|
errorMessage: response.error || 'Something went wrong',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setNewDashboardState({
|
||||||
|
...newDashboardState,
|
||||||
|
error: true,
|
||||||
|
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [newDashboardState, t]);
|
||||||
|
|
||||||
|
const getText = useCallback(() => {
|
||||||
|
if (!newDashboardState.error && !newDashboardState.loading) {
|
||||||
|
return 'New Dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDashboardState.loading) {
|
||||||
|
return 'Loading';
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDashboardState.errorMessage;
|
||||||
|
}, [
|
||||||
|
newDashboardState.error,
|
||||||
|
newDashboardState.errorMessage,
|
||||||
|
newDashboardState.loading,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||||
|
setIsImportJSONModalVisible((state) => !state);
|
||||||
|
setUploadedGrafana(uploadedGrafana);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMenuItems = useMemo(() => {
|
||||||
|
const menuItems: ItemType[] = [
|
||||||
|
{
|
||||||
|
key: t('import_json').toString(),
|
||||||
|
label: t('import_json'),
|
||||||
|
onClick: (): void => onModalHandler(false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t('import_grafana_json').toString(),
|
||||||
|
label: t('import_grafana_json'),
|
||||||
|
onClick: (): void => onModalHandler(true),
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (createNewDashboard) {
|
||||||
|
menuItems.unshift({
|
||||||
|
key: t('create_dashboard').toString(),
|
||||||
|
label: t('create_dashboard'),
|
||||||
|
disabled: isDashboardListLoading,
|
||||||
|
onClick: onNewDashboardHandler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return menuItems;
|
||||||
|
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
|
||||||
|
|
||||||
|
const searchArrayOfObjects = (searchValue: string): any[] => {
|
||||||
|
// Convert the searchValue to lowercase for case-insensitive search
|
||||||
|
const searchValueLowerCase = searchValue.toLowerCase();
|
||||||
|
|
||||||
|
// Use the filter method to find matching objects
|
||||||
|
return dashboardListResponse.filter((item: any) => {
|
||||||
|
// Convert each property value to lowercase for case-insensitive search
|
||||||
|
const itemValues = Object.values(item?.data).map((value: any) =>
|
||||||
|
value.toString().toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if any property value contains the searchValue
|
||||||
|
return itemValues.some((value) => value.includes(searchValueLowerCase));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = useDebouncedFn((event: unknown): void => {
|
||||||
|
setIsFilteringDashboards(true);
|
||||||
|
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||||
|
const filteredDashboards = searchArrayOfObjects(searchText);
|
||||||
|
setDashboards(filteredDashboards);
|
||||||
|
setIsFilteringDashboards(false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const GetHeader = useMemo(
|
||||||
|
() => (
|
||||||
|
<Row gutter={16} align="middle">
|
||||||
|
<Col span={18}>
|
||||||
|
<Search
|
||||||
|
disabled={isDashboardListLoading}
|
||||||
|
placeholder="Search by Name, Description, Tags"
|
||||||
|
onChange={handleSearch}
|
||||||
|
loading={isFilteringDashboards}
|
||||||
|
style={{ marginBottom: 16, marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col
|
||||||
|
span={6}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonContainer>
|
||||||
|
<TextToolTip
|
||||||
|
{...{
|
||||||
|
text: `More details on how to create dashboards`,
|
||||||
|
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonContainer>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items: getMenuItems }}
|
||||||
|
disabled={isDashboardListLoading}
|
||||||
|
placement="bottomRight"
|
||||||
|
>
|
||||||
|
<NewDashboardButton
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
type="primary"
|
||||||
|
data-testid="create-new-dashboard"
|
||||||
|
loading={newDashboardState.loading}
|
||||||
|
danger={newDashboardState.error}
|
||||||
|
>
|
||||||
|
{getText()}
|
||||||
|
</NewDashboardButton>
|
||||||
|
</Dropdown>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
isDashboardListLoading,
|
||||||
|
handleSearch,
|
||||||
|
isFilteringDashboards,
|
||||||
|
getMenuItems,
|
||||||
|
newDashboardState.loading,
|
||||||
|
newDashboardState.error,
|
||||||
|
getText,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
{GetHeader}
|
||||||
|
|
||||||
|
<TableContainer>
|
||||||
|
<ImportJSON
|
||||||
|
isImportJSONModalVisible={isImportJSONModalVisible}
|
||||||
|
uploadedGrafana={uploadedGrafana}
|
||||||
|
onModalHandler={(): void => onModalHandler(false)}
|
||||||
|
/>
|
||||||
|
<DynamicColumnTable
|
||||||
|
tablesource={TableDataSource.Dashboard}
|
||||||
|
dynamicColumns={dynamicColumns}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
defaultPageSize: 10,
|
||||||
|
total: data?.length || 0,
|
||||||
|
}}
|
||||||
|
showHeader
|
||||||
|
bordered
|
||||||
|
sticky
|
||||||
|
loading={isDashboardListLoading}
|
||||||
|
dataSource={data}
|
||||||
|
showSorterTooltip
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
key: Key;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
createdBy: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastUpdatedTime: string;
|
||||||
|
lastUpdatedBy: string;
|
||||||
|
isLocked: boolean;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardsList;
|
@ -2,7 +2,7 @@ import { Typography } from 'antd';
|
|||||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
||||||
import getFormattedDate from 'lib/getFormatedDate';
|
import getFormattedDate from 'lib/getFormatedDate';
|
||||||
|
|
||||||
import { Data } from '..';
|
import { Data } from '../DashboardsList';
|
||||||
|
|
||||||
function Created(createdBy: Data['createdBy']): JSX.Element {
|
function Created(createdBy: Data['createdBy']): JSX.Element {
|
||||||
const time = new Date(createdBy);
|
const time = new Date(createdBy);
|
||||||
|
@ -11,7 +11,7 @@ import { AppState } from 'store/reducers';
|
|||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
import { Data } from '..';
|
import { Data } from '../DashboardsList';
|
||||||
import { TableLinkText } from './styles';
|
import { TableLinkText } from './styles';
|
||||||
|
|
||||||
interface DeleteButtonProps {
|
interface DeleteButtonProps {
|
||||||
|
@ -2,7 +2,7 @@ import { LockFilled } from '@ant-design/icons';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
|
||||||
import { Data } from '..';
|
import { Data } from '../DashboardsList';
|
||||||
import { TableLinkText } from './styles';
|
import { TableLinkText } from './styles';
|
||||||
|
|
||||||
function Name(name: Data['name'], data: Data): JSX.Element {
|
function Name(name: Data['name'], data: Data): JSX.Element {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react/destructuring-assignment */
|
/* eslint-disable react/destructuring-assignment */
|
||||||
import { Tag } from 'antd';
|
import { Tag } from 'antd';
|
||||||
|
|
||||||
import { Data } from '../index';
|
import { Data } from '../DashboardsList';
|
||||||
|
|
||||||
function Tags(data: Data['tags']): JSX.Element {
|
function Tags(data: Data['tags']): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
@ -1,378 +1,3 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import DashboardsList from './DashboardsList';
|
||||||
import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
|
|
||||||
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
|
||||||
import createDashboard from 'api/dashboard/create';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import {
|
|
||||||
DynamicColumnsKey,
|
|
||||||
TableDataSource,
|
|
||||||
} from 'components/ResizeTable/contants';
|
|
||||||
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
|
||||||
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
|
||||||
import TextToolTip from 'components/TextToolTip';
|
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { generatePath } from 'react-router-dom';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
|
export default DashboardsList;
|
||||||
import ImportJSON from './ImportJSON';
|
|
||||||
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
|
|
||||||
import DeleteButton from './TableComponents/DeleteButton';
|
|
||||||
import Name from './TableComponents/Name';
|
|
||||||
|
|
||||||
const { Search } = Input;
|
|
||||||
|
|
||||||
function ListOfAllDashboard(): JSX.Element {
|
|
||||||
const {
|
|
||||||
data: dashboardListResponse = [],
|
|
||||||
isLoading: isDashboardListLoading,
|
|
||||||
refetch: refetchDashboardList,
|
|
||||||
} = useGetAllDashboard();
|
|
||||||
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
|
|
||||||
const [action, createNewDashboard] = useComponentPermission(
|
|
||||||
['action', 'create_new_dashboards'],
|
|
||||||
role,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { t } = useTranslation('dashboard');
|
|
||||||
|
|
||||||
const [
|
|
||||||
isImportJSONModalVisible,
|
|
||||||
setIsImportJSONModalVisible,
|
|
||||||
] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
|
||||||
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
|
|
||||||
|
|
||||||
const [dashboards, setDashboards] = useState<Dashboard[]>();
|
|
||||||
|
|
||||||
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
|
||||||
const sortedDashboards = dashboards.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
|
||||||
);
|
|
||||||
setDashboards(sortedDashboards);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
sortDashboardsByCreatedAt(dashboardListResponse);
|
|
||||||
}, [dashboardListResponse]);
|
|
||||||
|
|
||||||
const [newDashboardState, setNewDashboardState] = useState({
|
|
||||||
loading: false,
|
|
||||||
error: false,
|
|
||||||
errorMessage: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const dynamicColumns: TableColumnProps<Data>[] = [
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
width: 30,
|
|
||||||
key: DynamicColumnsKey.CreatedAt,
|
|
||||||
sorter: (a: Data, b: Data): number => {
|
|
||||||
console.log({ a });
|
|
||||||
const prev = new Date(a.createdAt).getTime();
|
|
||||||
const next = new Date(b.createdAt).getTime();
|
|
||||||
|
|
||||||
return prev - next;
|
|
||||||
},
|
|
||||||
render: DateComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created By',
|
|
||||||
dataIndex: 'createdBy',
|
|
||||||
width: 30,
|
|
||||||
key: DynamicColumnsKey.CreatedBy,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Last Updated Time',
|
|
||||||
width: 30,
|
|
||||||
dataIndex: 'lastUpdatedTime',
|
|
||||||
key: DynamicColumnsKey.UpdatedAt,
|
|
||||||
sorter: (a: Data, b: Data): number => {
|
|
||||||
const prev = new Date(a.lastUpdatedTime).getTime();
|
|
||||||
const next = new Date(b.lastUpdatedTime).getTime();
|
|
||||||
|
|
||||||
return prev - next;
|
|
||||||
},
|
|
||||||
render: DateComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Last Updated By',
|
|
||||||
dataIndex: 'lastUpdatedBy',
|
|
||||||
width: 30,
|
|
||||||
key: DynamicColumnsKey.UpdatedBy,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
|
||||||
const tableColumns: TableColumnProps<Data>[] = [
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
width: 40,
|
|
||||||
render: Name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Description',
|
|
||||||
width: 50,
|
|
||||||
dataIndex: 'description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Tags',
|
|
||||||
dataIndex: 'tags',
|
|
||||||
width: 50,
|
|
||||||
render: (value): JSX.Element => <LabelColumn labels={value} />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (action) {
|
|
||||||
tableColumns.push({
|
|
||||||
title: 'Action',
|
|
||||||
dataIndex: '',
|
|
||||||
width: 40,
|
|
||||||
render: DeleteButton,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return tableColumns;
|
|
||||||
}, [action]);
|
|
||||||
|
|
||||||
const data: Data[] =
|
|
||||||
dashboards?.map((e) => ({
|
|
||||||
createdAt: e.created_at,
|
|
||||||
description: e.data.description || '',
|
|
||||||
id: e.uuid,
|
|
||||||
lastUpdatedTime: e.updated_at,
|
|
||||||
name: e.data.title,
|
|
||||||
tags: e.data.tags || [],
|
|
||||||
key: e.uuid,
|
|
||||||
createdBy: e.created_by,
|
|
||||||
isLocked: !!e.isLocked || false,
|
|
||||||
lastUpdatedBy: e.updated_by,
|
|
||||||
refetchDashboardList,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const onNewDashboardHandler = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setNewDashboardState({
|
|
||||||
...newDashboardState,
|
|
||||||
loading: true,
|
|
||||||
});
|
|
||||||
const response = await createDashboard({
|
|
||||||
title: t('new_dashboard_title', {
|
|
||||||
ns: 'dashboard',
|
|
||||||
}),
|
|
||||||
uploadedGrafana: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
history.push(
|
|
||||||
generatePath(ROUTES.DASHBOARD, {
|
|
||||||
dashboardId: response.payload.uuid,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setNewDashboardState({
|
|
||||||
...newDashboardState,
|
|
||||||
loading: false,
|
|
||||||
error: true,
|
|
||||||
errorMessage: response.error || 'Something went wrong',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setNewDashboardState({
|
|
||||||
...newDashboardState,
|
|
||||||
error: true,
|
|
||||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [newDashboardState, t]);
|
|
||||||
|
|
||||||
const getText = useCallback(() => {
|
|
||||||
if (!newDashboardState.error && !newDashboardState.loading) {
|
|
||||||
return 'New Dashboard';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newDashboardState.loading) {
|
|
||||||
return 'Loading';
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDashboardState.errorMessage;
|
|
||||||
}, [
|
|
||||||
newDashboardState.error,
|
|
||||||
newDashboardState.errorMessage,
|
|
||||||
newDashboardState.loading,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
|
||||||
setIsImportJSONModalVisible((state) => !state);
|
|
||||||
setUploadedGrafana(uploadedGrafana);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMenuItems = useMemo(() => {
|
|
||||||
const menuItems: ItemType[] = [
|
|
||||||
{
|
|
||||||
key: t('import_json').toString(),
|
|
||||||
label: t('import_json'),
|
|
||||||
onClick: (): void => onModalHandler(false),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: t('import_grafana_json').toString(),
|
|
||||||
label: t('import_grafana_json'),
|
|
||||||
onClick: (): void => onModalHandler(true),
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (createNewDashboard) {
|
|
||||||
menuItems.unshift({
|
|
||||||
key: t('create_dashboard').toString(),
|
|
||||||
label: t('create_dashboard'),
|
|
||||||
disabled: isDashboardListLoading,
|
|
||||||
onClick: onNewDashboardHandler,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return menuItems;
|
|
||||||
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
|
|
||||||
|
|
||||||
const searchArrayOfObjects = (searchValue: string): any[] => {
|
|
||||||
// Convert the searchValue to lowercase for case-insensitive search
|
|
||||||
const searchValueLowerCase = searchValue.toLowerCase();
|
|
||||||
|
|
||||||
// Use the filter method to find matching objects
|
|
||||||
return dashboardListResponse.filter((item: any) => {
|
|
||||||
// Convert each property value to lowercase for case-insensitive search
|
|
||||||
const itemValues = Object.values(item?.data).map((value: any) =>
|
|
||||||
value.toString().toLowerCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if any property value contains the searchValue
|
|
||||||
return itemValues.some((value) => value.includes(searchValueLowerCase));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = useDebouncedFn((event: unknown): void => {
|
|
||||||
setIsFilteringDashboards(true);
|
|
||||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
|
||||||
const filteredDashboards = searchArrayOfObjects(searchText);
|
|
||||||
setDashboards(filteredDashboards);
|
|
||||||
setIsFilteringDashboards(false);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const GetHeader = useMemo(
|
|
||||||
() => (
|
|
||||||
<Row gutter={16} align="middle">
|
|
||||||
<Col span={18}>
|
|
||||||
<Search
|
|
||||||
disabled={isDashboardListLoading}
|
|
||||||
placeholder="Search by Name, Description, Tags"
|
|
||||||
onChange={handleSearch}
|
|
||||||
loading={isFilteringDashboards}
|
|
||||||
style={{ marginBottom: 16, marginTop: 16 }}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col
|
|
||||||
span={6}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonContainer>
|
|
||||||
<TextToolTip
|
|
||||||
{...{
|
|
||||||
text: `More details on how to create dashboards`,
|
|
||||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ButtonContainer>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
menu={{ items: getMenuItems }}
|
|
||||||
disabled={isDashboardListLoading}
|
|
||||||
placement="bottomRight"
|
|
||||||
>
|
|
||||||
<NewDashboardButton
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
type="primary"
|
|
||||||
data-testid="create-new-dashboard"
|
|
||||||
loading={newDashboardState.loading}
|
|
||||||
danger={newDashboardState.error}
|
|
||||||
>
|
|
||||||
{getText()}
|
|
||||||
</NewDashboardButton>
|
|
||||||
</Dropdown>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
),
|
|
||||||
[
|
|
||||||
isDashboardListLoading,
|
|
||||||
handleSearch,
|
|
||||||
isFilteringDashboards,
|
|
||||||
getMenuItems,
|
|
||||||
newDashboardState.loading,
|
|
||||||
newDashboardState.error,
|
|
||||||
getText,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
{GetHeader}
|
|
||||||
|
|
||||||
<TableContainer>
|
|
||||||
<ImportJSON
|
|
||||||
isImportJSONModalVisible={isImportJSONModalVisible}
|
|
||||||
uploadedGrafana={uploadedGrafana}
|
|
||||||
onModalHandler={(): void => onModalHandler(false)}
|
|
||||||
/>
|
|
||||||
<DynamicColumnTable
|
|
||||||
tablesource={TableDataSource.Dashboard}
|
|
||||||
dynamicColumns={dynamicColumns}
|
|
||||||
columns={columns}
|
|
||||||
pagination={{
|
|
||||||
pageSize: 10,
|
|
||||||
defaultPageSize: 10,
|
|
||||||
total: data?.length || 0,
|
|
||||||
}}
|
|
||||||
showHeader
|
|
||||||
bordered
|
|
||||||
sticky
|
|
||||||
loading={isDashboardListLoading}
|
|
||||||
dataSource={data}
|
|
||||||
showSorterTooltip
|
|
||||||
/>
|
|
||||||
</TableContainer>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Data {
|
|
||||||
key: Key;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
tags: string[];
|
|
||||||
createdBy: string;
|
|
||||||
createdAt: string;
|
|
||||||
lastUpdatedTime: string;
|
|
||||||
lastUpdatedBy: string;
|
|
||||||
isLocked: boolean;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ListOfAllDashboard;
|
|
||||||
|
@ -50,7 +50,7 @@ function DashboardDescription(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col flex={1} span={12}>
|
<Col flex={1} span={9}>
|
||||||
<Typography.Title
|
<Typography.Title
|
||||||
level={4}
|
level={4}
|
||||||
style={{ padding: 0, margin: 0 }}
|
style={{ padding: 0, margin: 0 }}
|
||||||
@ -80,12 +80,12 @@ function DashboardDescription(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={12}>
|
||||||
<Row justify="end">
|
<Row justify="end">
|
||||||
<DashboardVariableSelection />
|
<DashboardVariableSelection />
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4} style={{ textAlign: 'right' }}>
|
<Col span={3} style={{ textAlign: 'right' }}>
|
||||||
{selectedData && (
|
{selectedData && (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
isJSONModalVisible={openDashboardJSON}
|
isJSONModalVisible={openDashboardJSON}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
.query-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
@ -1,21 +1,16 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import './VariableItem.styles.scss';
|
||||||
|
|
||||||
import { orange } from '@ant-design/colors';
|
import { orange } from '@ant-design/colors';
|
||||||
import {
|
import { Button, Divider, Input, Select, Switch, Tag, Typography } from 'antd';
|
||||||
Button,
|
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||||
Col,
|
|
||||||
Divider,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
Tag,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import query from 'api/dashboard/variables/query';
|
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
import { map } from 'lodash-es';
|
import { map } from 'lodash-es';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import {
|
import {
|
||||||
IDashboardVariable,
|
IDashboardVariable,
|
||||||
TSortVariableValuesType,
|
TSortVariableValuesType,
|
||||||
@ -79,8 +74,6 @@ function VariableItem({
|
|||||||
);
|
);
|
||||||
const [previewValues, setPreviewValues] = useState<string[]>([]);
|
const [previewValues, setPreviewValues] = useState<string[]>([]);
|
||||||
|
|
||||||
// Internal states
|
|
||||||
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
|
|
||||||
// Error messages
|
// Error messages
|
||||||
const [errorName, setErrorName] = useState<boolean>(false);
|
const [errorName, setErrorName] = useState<boolean>(false);
|
||||||
const [errorPreview, setErrorPreview] = useState<string | null>(null);
|
const [errorPreview, setErrorPreview] = useState<string | null>(null);
|
||||||
@ -131,232 +124,268 @@ function VariableItem({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fetches the preview values for the SQL variable query
|
// Fetches the preview values for the SQL variable query
|
||||||
const handleQueryResult = async (): Promise<void> => {
|
const handleQueryResult = (response: any): void => {
|
||||||
setPreviewLoading(true);
|
if (response?.payload?.variableValues)
|
||||||
setErrorPreview(null);
|
setPreviewValues(
|
||||||
try {
|
sortValues(
|
||||||
const variableQueryResponse = await query({
|
response.payload?.variableValues || [],
|
||||||
query: variableQueryValue,
|
variableSortType,
|
||||||
variables: variablePropsToPayloadVariables(existingVariables),
|
) as never,
|
||||||
});
|
);
|
||||||
setPreviewLoading(false);
|
|
||||||
if (variableQueryResponse.error) {
|
|
||||||
let message = variableQueryResponse.error;
|
|
||||||
if (variableQueryResponse.error.includes('Syntax error:')) {
|
|
||||||
message =
|
|
||||||
'Please make sure query is valid and dependent variables are selected';
|
|
||||||
}
|
|
||||||
setErrorPreview(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (variableQueryResponse.payload?.variableValues)
|
|
||||||
setPreviewValues(
|
|
||||||
sortValues(
|
|
||||||
variableQueryResponse.payload?.variableValues || [],
|
|
||||||
variableSortType,
|
|
||||||
) as never,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { isFetching: previewLoading, refetch: runQuery } = useQuery(
|
||||||
|
[REACT_QUERY_KEY.DASHBOARD_BY_ID, variableData.name, variableName],
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
queryFn: () =>
|
||||||
|
dashboardVariablesQuery({
|
||||||
|
query: variableData.queryValue || '',
|
||||||
|
variables: variablePropsToPayloadVariables(existingVariables),
|
||||||
|
}),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
onSuccess: (response) => {
|
||||||
|
handleQueryResult(response);
|
||||||
|
},
|
||||||
|
onError: (error: {
|
||||||
|
details: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { details } = error;
|
||||||
|
|
||||||
|
if (details.error) {
|
||||||
|
let message = details.error;
|
||||||
|
if (details.error.includes('Syntax error:')) {
|
||||||
|
message =
|
||||||
|
'Please make sure query is valid and dependent variables are selected';
|
||||||
|
}
|
||||||
|
setErrorPreview(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTestRunQuery = useCallback(() => {
|
||||||
|
runQuery();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<div className="variable-item-container">
|
||||||
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
|
<div className="variable-item-content">
|
||||||
<VariableItemRow>
|
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
|
||||||
<LabelContainer>
|
<VariableItemRow>
|
||||||
<Typography>Name</Typography>
|
<LabelContainer>
|
||||||
</LabelContainer>
|
<Typography>Name</Typography>
|
||||||
<div>
|
</LabelContainer>
|
||||||
<Input
|
|
||||||
placeholder="Unique name of the variable"
|
|
||||||
style={{ width: 400 }}
|
|
||||||
value={variableName}
|
|
||||||
onChange={(e): void => {
|
|
||||||
setVariableName(e.target.value);
|
|
||||||
setErrorName(
|
|
||||||
!validateName(e.target.value) && e.target.value !== variableData.name,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text type="warning">
|
<Input
|
||||||
{errorName ? 'Variable name already exists' : ''}
|
placeholder="Unique name of the variable"
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VariableItemRow>
|
|
||||||
<VariableItemRow>
|
|
||||||
<LabelContainer>
|
|
||||||
<Typography>Description</Typography>
|
|
||||||
</LabelContainer>
|
|
||||||
|
|
||||||
<Input.TextArea
|
|
||||||
value={variableDescription}
|
|
||||||
placeholder="Write description of the variable"
|
|
||||||
style={{ width: 400 }}
|
|
||||||
onChange={(e): void => setVariableDescription(e.target.value)}
|
|
||||||
/>
|
|
||||||
</VariableItemRow>
|
|
||||||
<VariableItemRow>
|
|
||||||
<LabelContainer>
|
|
||||||
<Typography>Type</Typography>
|
|
||||||
</LabelContainer>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
defaultActiveFirstOption
|
|
||||||
style={{ width: 400 }}
|
|
||||||
onChange={(e: TVariableQueryType): void => {
|
|
||||||
setQueryType(e);
|
|
||||||
}}
|
|
||||||
value={queryType}
|
|
||||||
>
|
|
||||||
<Option value={VariableQueryTypeArr[0]}>Query</Option>
|
|
||||||
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
|
|
||||||
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
|
|
||||||
</Select>
|
|
||||||
</VariableItemRow>
|
|
||||||
<Typography.Title
|
|
||||||
level={5}
|
|
||||||
style={{ marginTop: '1rem', marginBottom: '1rem' }}
|
|
||||||
>
|
|
||||||
Options
|
|
||||||
</Typography.Title>
|
|
||||||
{queryType === 'QUERY' && (
|
|
||||||
<VariableItemRow>
|
|
||||||
<LabelContainer>
|
|
||||||
<Typography>Query</Typography>
|
|
||||||
</LabelContainer>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, position: 'relative' }}>
|
|
||||||
<Editor
|
|
||||||
language="sql"
|
|
||||||
value={variableQueryValue}
|
|
||||||
onChange={(e): void => setVariableQueryValue(e)}
|
|
||||||
height="300px"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleQueryResult}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
loading={previewLoading}
|
|
||||||
>
|
|
||||||
Test Run Query
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</VariableItemRow>
|
|
||||||
)}
|
|
||||||
{queryType === 'CUSTOM' && (
|
|
||||||
<VariableItemRow>
|
|
||||||
<LabelContainer>
|
|
||||||
<Typography>Values separated by comma</Typography>
|
|
||||||
</LabelContainer>
|
|
||||||
<Input.TextArea
|
|
||||||
value={variableCustomValue}
|
|
||||||
placeholder="1, 10, mykey, mykey:myvalue"
|
|
||||||
style={{ width: 400 }}
|
|
||||||
onChange={(e): void => {
|
|
||||||
setVariableCustomValue(e.target.value);
|
|
||||||
setPreviewValues(
|
|
||||||
sortValues(
|
|
||||||
commaValuesParser(e.target.value),
|
|
||||||
variableSortType,
|
|
||||||
) as never,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</VariableItemRow>
|
|
||||||
)}
|
|
||||||
{queryType === 'TEXTBOX' && (
|
|
||||||
<VariableItemRow>
|
|
||||||
<LabelContainer>
|
|
||||||
<Typography>Default Value</Typography>
|
|
||||||
</LabelContainer>
|
|
||||||
<Input
|
|
||||||
value={variableTextboxValue}
|
|
||||||
onChange={(e): void => {
|
|
||||||
setVariableTextboxValue(e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="Default value if any"
|
|
||||||
style={{ width: 400 }}
|
|
||||||
/>
|
|
||||||
</VariableItemRow>
|
|
||||||
)}
|
|
||||||
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
|
|
||||||
<>
|
|
||||||
<VariableItemRow>
|
|
||||||
<LabelContainer>
|
|
||||||
<Typography>Preview of Values</Typography>
|
|
||||||
</LabelContainer>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
{errorPreview ? (
|
|
||||||
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
|
|
||||||
) : (
|
|
||||||
map(previewValues, (value, idx) => (
|
|
||||||
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</VariableItemRow>
|
|
||||||
<VariableItemRow>
|
|
||||||
<LabelContainer>
|
|
||||||
<Typography>Sort</Typography>
|
|
||||||
</LabelContainer>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
defaultActiveFirstOption
|
|
||||||
style={{ width: 400 }}
|
style={{ width: 400 }}
|
||||||
defaultValue={VariableSortTypeArr[0]}
|
value={variableName}
|
||||||
value={variableSortType}
|
onChange={(e): void => {
|
||||||
onChange={(value: TSortVariableValuesType): void =>
|
setVariableName(e.target.value);
|
||||||
setVariableSortType(value)
|
setErrorName(
|
||||||
}
|
!validateName(e.target.value) && e.target.value !== variableData.name,
|
||||||
>
|
);
|
||||||
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
|
}}
|
||||||
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
|
/>
|
||||||
<Option value={VariableSortTypeArr[2]}>Descending</Option>
|
<div>
|
||||||
</Select>
|
<Typography.Text type="warning">
|
||||||
</VariableItemRow>
|
{errorName ? 'Variable name already exists' : ''}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VariableItemRow>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Description</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
|
||||||
|
<Input.TextArea
|
||||||
|
value={variableDescription}
|
||||||
|
placeholder="Write description of the variable"
|
||||||
|
style={{ width: 400 }}
|
||||||
|
onChange={(e): void => setVariableDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</VariableItemRow>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Type</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
defaultActiveFirstOption
|
||||||
|
style={{ width: 400 }}
|
||||||
|
onChange={(e: TVariableQueryType): void => {
|
||||||
|
setQueryType(e);
|
||||||
|
}}
|
||||||
|
value={queryType}
|
||||||
|
>
|
||||||
|
<Option value={VariableQueryTypeArr[0]}>Query</Option>
|
||||||
|
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
|
||||||
|
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
|
||||||
|
</Select>
|
||||||
|
</VariableItemRow>
|
||||||
|
<Typography.Title
|
||||||
|
level={5}
|
||||||
|
style={{ marginTop: '1rem', marginBottom: '1rem' }}
|
||||||
|
>
|
||||||
|
Options
|
||||||
|
</Typography.Title>
|
||||||
|
{queryType === 'QUERY' && (
|
||||||
|
<div className="query-container">
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Query</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, position: 'relative' }}>
|
||||||
|
<Editor
|
||||||
|
language="sql"
|
||||||
|
value={variableQueryValue}
|
||||||
|
onChange={(e): void => setVariableQueryValue(e)}
|
||||||
|
height="240px"
|
||||||
|
options={{
|
||||||
|
fontSize: 13,
|
||||||
|
wordWrap: 'on',
|
||||||
|
lineNumbers: 'off',
|
||||||
|
glyphMargin: false,
|
||||||
|
folding: false,
|
||||||
|
lineDecorationsWidth: 0,
|
||||||
|
lineNumbersMinChars: 0,
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={handleTestRunQuery}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
loading={previewLoading}
|
||||||
|
>
|
||||||
|
Test Run Query
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{queryType === 'CUSTOM' && (
|
||||||
<VariableItemRow>
|
<VariableItemRow>
|
||||||
<LabelContainer>
|
<LabelContainer>
|
||||||
<Typography>Enable multiple values to be checked</Typography>
|
<Typography>Values separated by comma</Typography>
|
||||||
</LabelContainer>
|
</LabelContainer>
|
||||||
<Switch
|
<Input.TextArea
|
||||||
checked={variableMultiSelect}
|
value={variableCustomValue}
|
||||||
|
placeholder="1, 10, mykey, mykey:myvalue"
|
||||||
|
style={{ width: 400 }}
|
||||||
onChange={(e): void => {
|
onChange={(e): void => {
|
||||||
setVariableMultiSelect(e);
|
setVariableCustomValue(e.target.value);
|
||||||
if (!e) {
|
setPreviewValues(
|
||||||
setVariableShowALLOption(false);
|
sortValues(
|
||||||
}
|
commaValuesParser(e.target.value),
|
||||||
|
variableSortType,
|
||||||
|
) as never,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</VariableItemRow>
|
</VariableItemRow>
|
||||||
{variableMultiSelect && (
|
)}
|
||||||
|
{queryType === 'TEXTBOX' && (
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Default Value</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
<Input
|
||||||
|
value={variableTextboxValue}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setVariableTextboxValue(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Default value if any"
|
||||||
|
style={{ width: 400 }}
|
||||||
|
/>
|
||||||
|
</VariableItemRow>
|
||||||
|
)}
|
||||||
|
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
|
||||||
|
<>
|
||||||
<VariableItemRow>
|
<VariableItemRow>
|
||||||
<LabelContainer>
|
<LabelContainer>
|
||||||
<Typography>Include an option for ALL values</Typography>
|
<Typography>Preview of Values</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
{errorPreview ? (
|
||||||
|
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
|
||||||
|
) : (
|
||||||
|
map(previewValues, (value, idx) => (
|
||||||
|
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</VariableItemRow>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Sort</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
defaultActiveFirstOption
|
||||||
|
style={{ width: 400 }}
|
||||||
|
defaultValue={VariableSortTypeArr[0]}
|
||||||
|
value={variableSortType}
|
||||||
|
onChange={(value: TSortVariableValuesType): void =>
|
||||||
|
setVariableSortType(value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
|
||||||
|
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
|
||||||
|
<Option value={VariableSortTypeArr[2]}>Descending</Option>
|
||||||
|
</Select>
|
||||||
|
</VariableItemRow>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Enable multiple values to be checked</Typography>
|
||||||
</LabelContainer>
|
</LabelContainer>
|
||||||
<Switch
|
<Switch
|
||||||
checked={variableShowALLOption}
|
checked={variableMultiSelect}
|
||||||
onChange={(e): void => setVariableShowALLOption(e)}
|
onChange={(e): void => {
|
||||||
|
setVariableMultiSelect(e);
|
||||||
|
if (!e) {
|
||||||
|
setVariableShowALLOption(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</VariableItemRow>
|
</VariableItemRow>
|
||||||
)}
|
{variableMultiSelect && (
|
||||||
</>
|
<VariableItemRow>
|
||||||
)}
|
<LabelContainer>
|
||||||
<Divider />
|
<Typography>Include an option for ALL values</Typography>
|
||||||
<VariableItemRow>
|
</LabelContainer>
|
||||||
<Button type="primary" onClick={handleSave} disabled={errorName}>
|
<Switch
|
||||||
Save
|
checked={variableShowALLOption}
|
||||||
</Button>
|
onChange={(e): void => setVariableShowALLOption(e)}
|
||||||
<Button type="dashed" onClick={onCancel}>
|
/>
|
||||||
Cancel
|
</VariableItemRow>
|
||||||
</Button>
|
)}
|
||||||
</VariableItemRow>
|
</>
|
||||||
</Col>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="variable-item-footer">
|
||||||
|
<Divider />
|
||||||
|
<VariableItemRow>
|
||||||
|
<Button type="primary" onClick={handleSave} disabled={errorName}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button type="default" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</VariableItemRow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { Button, Modal, Row, Space, Tag } from 'antd';
|
|||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { PencilIcon, TrashIcon } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -134,7 +135,7 @@ function VariablesSetting(): JSX.Element {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Definition',
|
title: 'Description',
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
width: 100,
|
width: 100,
|
||||||
key: 'description',
|
key: 'description',
|
||||||
@ -147,19 +148,19 @@ function VariablesSetting(): JSX.Element {
|
|||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
style={{ padding: 0, cursor: 'pointer', color: blue[5] }}
|
style={{ padding: 8, cursor: 'pointer', color: blue[5] }}
|
||||||
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
|
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
|
||||||
>
|
>
|
||||||
Edit
|
<PencilIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
style={{ padding: 0, color: red[6], cursor: 'pointer' }}
|
style={{ padding: 8, color: red[6], cursor: 'pointer' }}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
if (_.name) onVariableDeleteHandler(_.name);
|
if (_.name) onVariableDeleteHandler(_.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
<TrashIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@ -187,9 +188,10 @@ function VariablesSetting(): JSX.Element {
|
|||||||
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
|
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> New Variables
|
<PlusOutlined /> Add Variable
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<ResizeTable columns={columns} dataSource={variablesTableData} />
|
<ResizeTable columns={columns} dataSource={variablesTableData} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -13,7 +13,7 @@ function DashboardSettingsContent(): JSX.Element {
|
|||||||
{ label: 'Variables', key: 'variables', children: <VariablesSetting /> },
|
{ label: 'Variables', key: 'variables', children: <VariablesSetting /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
return <Tabs items={items} />;
|
return <Tabs items={items} animated />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DashboardSettingsContent;
|
export default DashboardSettingsContent;
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
.variable-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: gray;
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
import { Row } from 'antd';
|
||||||
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { map, sortBy } from 'lodash-es';
|
||||||
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import VariableItem from './VariableItem';
|
||||||
|
|
||||||
|
function DashboardVariableSelection(): JSX.Element | null {
|
||||||
|
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||||
|
|
||||||
|
const { data } = selectedDashboard || {};
|
||||||
|
|
||||||
|
const { variables } = data || {};
|
||||||
|
|
||||||
|
const [update, setUpdate] = useState<boolean>(false);
|
||||||
|
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||||
|
|
||||||
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const onVarChanged = (name: string): void => {
|
||||||
|
setLastUpdatedVar(name);
|
||||||
|
setUpdate(!update);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMutation = useUpdateDashboard();
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const updateVariables = (
|
||||||
|
name: string,
|
||||||
|
updatedVariablesData: Dashboard['data']['variables'],
|
||||||
|
): void => {
|
||||||
|
if (!selectedDashboard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMutation.mutateAsync(
|
||||||
|
{
|
||||||
|
...selectedDashboard,
|
||||||
|
data: {
|
||||||
|
...selectedDashboard.data,
|
||||||
|
variables: updatedVariablesData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (updatedDashboard) => {
|
||||||
|
if (updatedDashboard.payload) {
|
||||||
|
setSelectedDashboard(updatedDashboard.payload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
notifications.error({
|
||||||
|
message: `Error updating ${name} variable`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onValueUpdate = (
|
||||||
|
name: string,
|
||||||
|
value: IDashboardVariable['selectedValue'],
|
||||||
|
allSelected: boolean,
|
||||||
|
): void => {
|
||||||
|
const updatedVariablesData = { ...variables };
|
||||||
|
updatedVariablesData[name].selectedValue = value;
|
||||||
|
updatedVariablesData[name].allSelected = allSelected;
|
||||||
|
|
||||||
|
console.log('onValue Update', name);
|
||||||
|
|
||||||
|
if (role !== 'VIEWER' && selectedDashboard) {
|
||||||
|
updateVariables(name, updatedVariablesData);
|
||||||
|
}
|
||||||
|
onVarChanged(name);
|
||||||
|
|
||||||
|
setUpdate(!update);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!variables) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variablesKeys = sortBy(Object.keys(variables));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
{variablesKeys &&
|
||||||
|
map(variablesKeys, (variableName) => (
|
||||||
|
<VariableItem
|
||||||
|
key={`${variableName}${variables[variableName].modificationUUID}`}
|
||||||
|
existingVariables={variables}
|
||||||
|
lastUpdatedVar={lastUpdatedVar}
|
||||||
|
variableData={{
|
||||||
|
name: variableName,
|
||||||
|
...variables[variableName],
|
||||||
|
change: update,
|
||||||
|
}}
|
||||||
|
onValueUpdate={onValueUpdate}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(DashboardVariableSelection);
|
@ -1,6 +1,13 @@
|
|||||||
import '@testing-library/jest-dom/extend-expect';
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
@ -25,7 +32,6 @@ const mockCustomVariableData: IDashboardVariable = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockOnValueUpdate = jest.fn();
|
const mockOnValueUpdate = jest.fn();
|
||||||
const mockOnAllSelectedUpdate = jest.fn();
|
|
||||||
|
|
||||||
describe('VariableItem', () => {
|
describe('VariableItem', () => {
|
||||||
let useEffectSpy: jest.SpyInstance;
|
let useEffectSpy: jest.SpyInstance;
|
||||||
@ -41,13 +47,14 @@ describe('VariableItem', () => {
|
|||||||
|
|
||||||
test('renders component with default props', () => {
|
test('renders component with default props', () => {
|
||||||
render(
|
render(
|
||||||
<VariableItem
|
<MockQueryClientProvider>
|
||||||
variableData={mockVariableData}
|
<VariableItem
|
||||||
existingVariables={{}}
|
variableData={mockVariableData}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
existingVariables={{}}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('$testVariable')).toBeInTheDocument();
|
expect(screen.getByText('$testVariable')).toBeInTheDocument();
|
||||||
@ -55,45 +62,55 @@ describe('VariableItem', () => {
|
|||||||
|
|
||||||
test('renders Input when the variable type is TEXTBOX', () => {
|
test('renders Input when the variable type is TEXTBOX', () => {
|
||||||
render(
|
render(
|
||||||
<VariableItem
|
<MockQueryClientProvider>
|
||||||
variableData={mockVariableData}
|
<VariableItem
|
||||||
existingVariables={{}}
|
variableData={mockVariableData}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
existingVariables={{}}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls onChange event handler when Input value changes', () => {
|
test('calls onChange event handler when Input value changes', async () => {
|
||||||
render(
|
render(
|
||||||
<VariableItem
|
<MockQueryClientProvider>
|
||||||
variableData={mockVariableData}
|
<VariableItem
|
||||||
existingVariables={{}}
|
variableData={mockVariableData}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
existingVariables={{}}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
const inputElement = screen.getByPlaceholderText('Enter value');
|
|
||||||
fireEvent.change(inputElement, { target: { value: 'newValue' } });
|
|
||||||
|
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
act(() => {
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledWith('testVariable', 'newValue');
|
const inputElement = screen.getByPlaceholderText('Enter value');
|
||||||
expect(mockOnAllSelectedUpdate).toHaveBeenCalledTimes(1);
|
fireEvent.change(inputElement, { target: { value: 'newValue' } });
|
||||||
expect(mockOnAllSelectedUpdate).toHaveBeenCalledWith('testVariable', false);
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||||
|
'testVariable',
|
||||||
|
'newValue',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders a Select element when variable type is CUSTOM', () => {
|
test('renders a Select element when variable type is CUSTOM', () => {
|
||||||
render(
|
render(
|
||||||
<VariableItem
|
<MockQueryClientProvider>
|
||||||
variableData={mockCustomVariableData}
|
<VariableItem
|
||||||
existingVariables={{}}
|
variableData={mockCustomVariableData}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
existingVariables={{}}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('$customVariable')).toBeInTheDocument();
|
expect(screen.getByText('$customVariable')).toBeInTheDocument();
|
||||||
@ -107,13 +124,14 @@ describe('VariableItem', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<VariableItem
|
<MockQueryClientProvider>
|
||||||
variableData={customVariableData}
|
<VariableItem
|
||||||
existingVariables={{}}
|
variableData={customVariableData}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
existingVariables={{}}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTitle('ALL')).toBeInTheDocument();
|
expect(screen.getByTitle('ALL')).toBeInTheDocument();
|
||||||
@ -121,48 +139,16 @@ describe('VariableItem', () => {
|
|||||||
|
|
||||||
test('calls useEffect when the component mounts', () => {
|
test('calls useEffect when the component mounts', () => {
|
||||||
render(
|
render(
|
||||||
<VariableItem
|
<MockQueryClientProvider>
|
||||||
variableData={mockCustomVariableData}
|
<VariableItem
|
||||||
existingVariables={{}}
|
variableData={mockCustomVariableData}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
existingVariables={{}}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(useEffect).toHaveBeenCalled();
|
expect(useEffect).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls useEffect only once when the component mounts', () => {
|
|
||||||
// Render the component
|
|
||||||
const { rerender } = render(
|
|
||||||
<VariableItem
|
|
||||||
variableData={mockCustomVariableData}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create an updated version of the mock data
|
|
||||||
const updatedMockCustomVariableData = {
|
|
||||||
...mockCustomVariableData,
|
|
||||||
selectedValue: 'option1',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Re-render the component with the updated data
|
|
||||||
rerender(
|
|
||||||
<VariableItem
|
|
||||||
variableData={updatedMockCustomVariableData}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if the useEffect is called with the correct arguments
|
|
||||||
expect(useEffectSpy).toHaveBeenCalledTimes(4);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,27 +1,35 @@
|
|||||||
|
import './DashboardVariableSelection.styles.scss';
|
||||||
|
|
||||||
import { orange } from '@ant-design/colors';
|
import { orange } from '@ant-design/colors';
|
||||||
import { WarningOutlined } from '@ant-design/icons';
|
import { WarningOutlined } from '@ant-design/icons';
|
||||||
import { Input, Popover, Select, Typography } from 'antd';
|
import { Input, Popover, Select, Typography } from 'antd';
|
||||||
import query from 'api/dashboard/variables/query';
|
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import useDebounce from 'hooks/useDebounce';
|
||||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
import map from 'lodash-es/map';
|
import map from 'lodash-es/map';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||||
|
|
||||||
import { variablePropsToPayloadVariables } from '../utils';
|
import { variablePropsToPayloadVariables } from '../utils';
|
||||||
import { SelectItemStyle, VariableContainer, VariableName } from './styles';
|
import { SelectItemStyle, VariableContainer, VariableValue } from './styles';
|
||||||
import { areArraysEqual } from './util';
|
import { areArraysEqual } from './util';
|
||||||
|
|
||||||
const ALL_SELECT_VALUE = '__ALL__';
|
const ALL_SELECT_VALUE = '__ALL__';
|
||||||
|
|
||||||
|
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
|
||||||
|
|
||||||
interface VariableItemProps {
|
interface VariableItemProps {
|
||||||
variableData: IDashboardVariable;
|
variableData: IDashboardVariable;
|
||||||
existingVariables: Record<string, IDashboardVariable>;
|
existingVariables: Record<string, IDashboardVariable>;
|
||||||
onValueUpdate: (
|
onValueUpdate: (
|
||||||
name: string,
|
name: string,
|
||||||
arg1: IDashboardVariable['selectedValue'],
|
arg1: IDashboardVariable['selectedValue'],
|
||||||
|
allSelected: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
onAllSelectedUpdate: (name: string, arg1: boolean) => void;
|
|
||||||
lastUpdatedVar: string;
|
lastUpdatedVar: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,48 +46,74 @@ function VariableItem({
|
|||||||
variableData,
|
variableData,
|
||||||
existingVariables,
|
existingVariables,
|
||||||
onValueUpdate,
|
onValueUpdate,
|
||||||
onAllSelectedUpdate,
|
|
||||||
lastUpdatedVar,
|
lastUpdatedVar,
|
||||||
}: VariableItemProps): JSX.Element {
|
}: VariableItemProps): JSX.Element {
|
||||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
|
const [variableValue, setVaribleValue] = useState(
|
||||||
|
variableData?.selectedValue?.toString() || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedVariableValue = useDebounce(variableValue, 500);
|
||||||
|
|
||||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||||
|
|
||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
useEffect(() => {
|
||||||
const getOptions = useCallback(async (): Promise<void> => {
|
const { selectedValue } = variableData;
|
||||||
if (variableData.type === 'QUERY') {
|
|
||||||
|
if (selectedValue) {
|
||||||
|
setVaribleValue(selectedValue?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [variableData]);
|
||||||
|
|
||||||
|
const getDependentVariables = (queryValue: string): string[] => {
|
||||||
|
const matches = queryValue.match(variableRegexPattern);
|
||||||
|
|
||||||
|
// Extract variable names from the matches array without {{ . }}
|
||||||
|
return matches
|
||||||
|
? matches.map((match) => match.replace(variableRegexPattern, '$1'))
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQueryKey = (variableData: IDashboardVariable): string[] => {
|
||||||
|
let dependentVariablesStr = '';
|
||||||
|
|
||||||
|
const dependentVariables = getDependentVariables(
|
||||||
|
variableData.queryValue || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const variableName = variableData.name || '';
|
||||||
|
|
||||||
|
dependentVariables?.forEach((element) => {
|
||||||
|
dependentVariablesStr += `${element}${existingVariables[element]?.selectedValue}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const variableKey = dependentVariablesStr.replace(/\s/g, '');
|
||||||
|
|
||||||
|
return [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableName, variableKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const getOptions = (variablesRes: VariableResponseProps | null): void => {
|
||||||
|
if (variablesRes && variableData.type === 'QUERY') {
|
||||||
try {
|
try {
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const response = await query({
|
if (
|
||||||
query: variableData.queryValue || '',
|
variablesRes?.variableValues &&
|
||||||
variables: variablePropsToPayloadVariables(existingVariables),
|
Array.isArray(variablesRes?.variableValues)
|
||||||
});
|
) {
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
if (response.error) {
|
|
||||||
let message = response.error;
|
|
||||||
if (response.error.includes('Syntax error:')) {
|
|
||||||
message =
|
|
||||||
'Please make sure query is valid and dependent variables are selected';
|
|
||||||
}
|
|
||||||
setErrorMessage(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response.payload?.variableValues) {
|
|
||||||
const newOptionsData = sortValues(
|
const newOptionsData = sortValues(
|
||||||
response.payload?.variableValues,
|
variablesRes?.variableValues,
|
||||||
variableData.sort,
|
variableData.sort,
|
||||||
);
|
);
|
||||||
// Since there is a chance of a variable being dependent on other
|
|
||||||
// variables, we need to check if the optionsData has changed
|
|
||||||
// If it has changed, we need to update the dependent variable
|
|
||||||
// So we compare the new optionsData with the old optionsData
|
|
||||||
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
||||||
|
|
||||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||||
/* eslint-disable no-useless-escape */
|
/* eslint-disable no-useless-escape */
|
||||||
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
|
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
|
||||||
@ -104,10 +138,10 @@ function VariableItem({
|
|||||||
[value] = newOptionsData;
|
[value] = newOptionsData;
|
||||||
}
|
}
|
||||||
if (variableData.name) {
|
if (variableData.name) {
|
||||||
onValueUpdate(variableData.name, value);
|
onValueUpdate(variableData.name, value, allSelected);
|
||||||
onAllSelectedUpdate(variableData.name, allSelected);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOptionsData(newOptionsData);
|
setOptionsData(newOptionsData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,19 +156,37 @@ function VariableItem({
|
|||||||
) as never,
|
) as never,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
|
||||||
variableData,
|
|
||||||
existingVariables,
|
|
||||||
onValueUpdate,
|
|
||||||
onAllSelectedUpdate,
|
|
||||||
optionsData,
|
|
||||||
lastUpdatedVar,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getOptions();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [variableData, existingVariables]);
|
};
|
||||||
|
|
||||||
|
const { isLoading } = useQuery(getQueryKey(variableData), {
|
||||||
|
enabled: variableData && variableData.type === 'QUERY',
|
||||||
|
queryFn: () =>
|
||||||
|
dashboardVariablesQuery({
|
||||||
|
query: variableData.queryValue || '',
|
||||||
|
variables: variablePropsToPayloadVariables(existingVariables),
|
||||||
|
}),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
onSuccess: (response) => {
|
||||||
|
getOptions(response.payload);
|
||||||
|
},
|
||||||
|
onError: (error: {
|
||||||
|
details: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { details } = error;
|
||||||
|
|
||||||
|
if (details.error) {
|
||||||
|
let message = details.error;
|
||||||
|
if (details.error.includes('Syntax error:')) {
|
||||||
|
message =
|
||||||
|
'Please make sure query is valid and dependent variables are selected';
|
||||||
|
}
|
||||||
|
setErrorMessage(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleChange = (value: string | string[]): void => {
|
const handleChange = (value: string | string[]): void => {
|
||||||
if (variableData.name)
|
if (variableData.name)
|
||||||
@ -143,11 +195,9 @@ function VariableItem({
|
|||||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
||||||
(Array.isArray(value) && value.length === 0)
|
(Array.isArray(value) && value.length === 0)
|
||||||
) {
|
) {
|
||||||
onValueUpdate(variableData.name, optionsData);
|
onValueUpdate(variableData.name, optionsData, true);
|
||||||
onAllSelectedUpdate(variableData.name, true);
|
|
||||||
} else {
|
} else {
|
||||||
onValueUpdate(variableData.name, value);
|
onValueUpdate(variableData.name, value, false);
|
||||||
onAllSelectedUpdate(variableData.name, false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -165,61 +215,78 @@ function VariableItem({
|
|||||||
? 'multiple'
|
? 'multiple'
|
||||||
: undefined;
|
: undefined;
|
||||||
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedVariableValue !== variableData?.selectedValue?.toString()) {
|
||||||
|
handleChange(debouncedVariableValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [debouncedVariableValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VariableContainer>
|
<VariableContainer>
|
||||||
<VariableName>${variableData.name}</VariableName>
|
<Typography.Text className="variable-name" ellipsis>
|
||||||
{variableData.type === 'TEXTBOX' ? (
|
${variableData.name}
|
||||||
<Input
|
</Typography.Text>
|
||||||
placeholder="Enter value"
|
<VariableValue>
|
||||||
bordered={false}
|
{variableData.type === 'TEXTBOX' ? (
|
||||||
value={variableData.selectedValue?.toString()}
|
<Input
|
||||||
onChange={(e): void => {
|
placeholder="Enter value"
|
||||||
handleChange(e.target.value || '');
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width:
|
|
||||||
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
!errorMessage && (
|
|
||||||
<Select
|
|
||||||
value={selectValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
bordered={false}
|
bordered={false}
|
||||||
placeholder="Select value"
|
value={variableValue}
|
||||||
mode={mode}
|
onChange={(e): void => {
|
||||||
dropdownMatchSelectWidth={false}
|
setVaribleValue(e.target.value || '');
|
||||||
style={SelectItemStyle}
|
}}
|
||||||
loading={isLoading}
|
style={{
|
||||||
showArrow
|
width:
|
||||||
showSearch
|
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
|
||||||
data-testid="variable-select"
|
}}
|
||||||
>
|
/>
|
||||||
{enableSelectAll && (
|
) : (
|
||||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
!errorMessage &&
|
||||||
ALL
|
optionsData && (
|
||||||
</Select.Option>
|
<Select
|
||||||
)}
|
value={selectValue}
|
||||||
{map(optionsData, (option) => (
|
onChange={handleChange}
|
||||||
<Select.Option
|
bordered={false}
|
||||||
data-testid={`option-${option}`}
|
placeholder="Select value"
|
||||||
key={option.toString()}
|
mode={mode}
|
||||||
value={option}
|
dropdownMatchSelectWidth={false}
|
||||||
>
|
style={SelectItemStyle}
|
||||||
{option.toString()}
|
loading={isLoading}
|
||||||
</Select.Option>
|
showArrow
|
||||||
))}
|
showSearch
|
||||||
</Select>
|
data-testid="variable-select"
|
||||||
)
|
>
|
||||||
)}
|
{enableSelectAll && (
|
||||||
{errorMessage && (
|
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||||
<span style={{ margin: '0 0.5rem' }}>
|
ALL
|
||||||
<Popover placement="top" content={<Typography>{errorMessage}</Typography>}>
|
</Select.Option>
|
||||||
<WarningOutlined style={{ color: orange[5] }} />
|
)}
|
||||||
</Popover>
|
{map(optionsData, (option) => (
|
||||||
</span>
|
<Select.Option
|
||||||
)}
|
data-testid={`option-${option}`}
|
||||||
|
key={option.toString()}
|
||||||
|
value={option}
|
||||||
|
>
|
||||||
|
{option.toString()}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{errorMessage && (
|
||||||
|
<span style={{ margin: '0 0.5rem' }}>
|
||||||
|
<Popover
|
||||||
|
placement="top"
|
||||||
|
content={<Typography>{errorMessage}</Typography>}
|
||||||
|
>
|
||||||
|
<WarningOutlined style={{ color: orange[5] }} />
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</VariableValue>
|
||||||
</VariableContainer>
|
</VariableContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,117 +1,3 @@
|
|||||||
import { Row } from 'antd';
|
import DashboardVariableSelection from './DashboardVariableSelection';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { map, sortBy } from 'lodash-es';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { memo, useState } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import VariableItem from './VariableItem';
|
export default DashboardVariableSelection;
|
||||||
|
|
||||||
function DashboardVariableSelection(): JSX.Element | null {
|
|
||||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
|
||||||
|
|
||||||
const { data } = selectedDashboard || {};
|
|
||||||
|
|
||||||
const { variables } = data || {};
|
|
||||||
|
|
||||||
const [update, setUpdate] = useState<boolean>(false);
|
|
||||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
|
||||||
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
|
|
||||||
const onVarChanged = (name: string): void => {
|
|
||||||
setLastUpdatedVar(name);
|
|
||||||
setUpdate(!update);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMutation = useUpdateDashboard();
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const updateVariables = (
|
|
||||||
updatedVariablesData: Dashboard['data']['variables'],
|
|
||||||
): void => {
|
|
||||||
if (!selectedDashboard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMutation.mutateAsync(
|
|
||||||
{
|
|
||||||
...selectedDashboard,
|
|
||||||
data: {
|
|
||||||
...selectedDashboard.data,
|
|
||||||
variables: updatedVariablesData,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: (updatedDashboard) => {
|
|
||||||
if (updatedDashboard.payload) {
|
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
|
||||||
notifications.success({
|
|
||||||
message: 'Variable updated successfully',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
notifications.error({
|
|
||||||
message: 'Error while updating variable',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onValueUpdate = (
|
|
||||||
name: string,
|
|
||||||
value: IDashboardVariable['selectedValue'],
|
|
||||||
): void => {
|
|
||||||
const updatedVariablesData = { ...variables };
|
|
||||||
updatedVariablesData[name].selectedValue = value;
|
|
||||||
|
|
||||||
if (role !== 'VIEWER' && selectedDashboard) {
|
|
||||||
updateVariables(updatedVariablesData);
|
|
||||||
}
|
|
||||||
|
|
||||||
onVarChanged(name);
|
|
||||||
};
|
|
||||||
const onAllSelectedUpdate = (
|
|
||||||
name: string,
|
|
||||||
value: IDashboardVariable['allSelected'],
|
|
||||||
): void => {
|
|
||||||
const updatedVariablesData = { ...variables };
|
|
||||||
updatedVariablesData[name].allSelected = value;
|
|
||||||
|
|
||||||
if (role !== 'VIEWER') {
|
|
||||||
updateVariables(updatedVariablesData);
|
|
||||||
}
|
|
||||||
onVarChanged(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!variables) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
{map(sortBy(Object.keys(variables)), (variableName) => (
|
|
||||||
<VariableItem
|
|
||||||
key={`${variableName}${variables[variableName].modificationUUID}`}
|
|
||||||
existingVariables={variables}
|
|
||||||
variableData={{
|
|
||||||
name: variableName,
|
|
||||||
...variables[variableName],
|
|
||||||
change: update,
|
|
||||||
}}
|
|
||||||
onValueUpdate={onValueUpdate}
|
|
||||||
onAllSelectedUpdate={onAllSelectedUpdate}
|
|
||||||
lastUpdatedVar={lastUpdatedVar}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(DashboardVariableSelection);
|
|
||||||
|
@ -3,19 +3,40 @@ import { Typography } from 'antd';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const VariableContainer = styled.div`
|
export const VariableContainer = styled.div`
|
||||||
|
max-width: 100%;
|
||||||
border: 1px solid ${grey[1]}66;
|
border: 1px solid ${grey[1]}66;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
|
margin-right: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const VariableName = styled(Typography)`
|
export const VariableName = styled(Typography)`
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-style: italic;
|
|
||||||
color: ${grey[0]};
|
color: ${grey[0]};
|
||||||
|
|
||||||
|
min-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VariableValue = styled(Typography)`
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: ${grey[0]};
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 300px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SelectItemStyle = {
|
export const SelectItemStyle = {
|
||||||
|
@ -68,11 +68,7 @@ export const getOptions = (routes: string): Option[] => {
|
|||||||
return Options;
|
return Options;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routesToHideBreadCrumbs = [
|
export const routesToHideBreadCrumbs = [ROUTES.SUPPORT, ROUTES.ALL_DASHBOARD];
|
||||||
ROUTES.SUPPORT,
|
|
||||||
ROUTES.ALL_DASHBOARD,
|
|
||||||
ROUTES.DASHBOARD,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const routesToSkip = [
|
export const routesToSkip = [
|
||||||
ROUTES.SETTINGS,
|
ROUTES.SETTINGS,
|
||||||
|
@ -3,7 +3,7 @@ import ReleaseNote from 'components/ReleaseNote';
|
|||||||
import ListOfAllDashboard from 'container/ListOfDashboard';
|
import ListOfAllDashboard from 'container/ListOfDashboard';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
function Dashboard(): JSX.Element {
|
function DashboardsListPage(): JSX.Element {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -14,4 +14,4 @@ function Dashboard(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard;
|
export default DashboardsListPage;
|
3
frontend/src/pages/DashboardsListPage/index.tsx
Normal file
3
frontend/src/pages/DashboardsListPage/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import DashboardsListPage from './DashboardsListPage';
|
||||||
|
|
||||||
|
export default DashboardsListPage;
|
33
frontend/src/pages/NewDashboard/DashboardPage.tsx
Normal file
33
frontend/src/pages/NewDashboard/DashboardPage.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import NotFound from 'components/NotFound';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import NewDashboard from 'container/NewDashboard';
|
||||||
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { ErrorType } from 'types/common';
|
||||||
|
|
||||||
|
function DashboardPage(): JSX.Element {
|
||||||
|
const { dashboardResponse } = useDashboard();
|
||||||
|
|
||||||
|
const { isFetching, isError, isLoading } = dashboardResponse;
|
||||||
|
|
||||||
|
const errorMessage = isError
|
||||||
|
? (dashboardResponse?.error as AxiosError)?.response?.data.errorType
|
||||||
|
: 'Something went wrong';
|
||||||
|
|
||||||
|
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
||||||
|
return <NotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError && errorMessage) {
|
||||||
|
return <Typography>{errorMessage}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner tip="Loading.." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NewDashboard />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardPage;
|
@ -1,33 +1,3 @@
|
|||||||
import { Typography } from 'antd';
|
import DashboardPage from './DashboardPage';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import NotFound from 'components/NotFound';
|
|
||||||
import Spinner from 'components/Spinner';
|
|
||||||
import NewDashboard from 'container/NewDashboard';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { ErrorType } from 'types/common';
|
|
||||||
|
|
||||||
function NewDashboardPage(): JSX.Element {
|
export default DashboardPage;
|
||||||
const { dashboardResponse } = useDashboard();
|
|
||||||
|
|
||||||
const { isFetching, isError, isLoading } = dashboardResponse;
|
|
||||||
|
|
||||||
const errorMessage = isError
|
|
||||||
? (dashboardResponse?.error as AxiosError)?.response?.data.errorType
|
|
||||||
: 'Something went wrong';
|
|
||||||
|
|
||||||
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
|
||||||
return <NotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError && errorMessage) {
|
|
||||||
return <Typography>{errorMessage}</Typography>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Spinner tip="Loading.." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <NewDashboard />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewDashboardPage;
|
|
||||||
|
@ -10,6 +10,6 @@ export type Props = {
|
|||||||
variables: PayloadVariables;
|
variables: PayloadVariables;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PayloadProps = {
|
export type VariableResponseProps = {
|
||||||
variableValues: string[] | number[];
|
variableValues: string[] | number[];
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"sourceMap": true,
|
||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
@ -20,11 +21,12 @@
|
|||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"plugins": [{ "name": "typescript-plugin-css-modules" }],
|
"plugins": [{ "name": "typescript-plugin-css-modules" }],
|
||||||
"types": ["node", "jest"]
|
"types": ["node", "jest"],
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
"include": [
|
"include": [
|
||||||
"./src",
|
"./src",
|
||||||
|
"./src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts",
|
||||||
"./babel.config.js",
|
"./babel.config.js",
|
||||||
"./jest.config.ts",
|
"./jest.config.ts",
|
||||||
"./.eslintrc.js",
|
"./.eslintrc.js",
|
||||||
|
@ -46,7 +46,7 @@ if (process.env.BUNDLE_ANALYSER === 'true') {
|
|||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
devtool: 'source-map',
|
devtool: 'eval-source-map',
|
||||||
entry: resolve(__dirname, './src/index.tsx'),
|
entry: resolve(__dirname, './src/index.tsx'),
|
||||||
devServer: {
|
devServer: {
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user