From b2d6d75eefaea54dbc18f6935098da3bbc0367aa Mon Sep 17 00:00:00 2001 From: Yunus M Date: Thu, 30 Nov 2023 13:56:49 +0530 Subject: [PATCH] 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 --- frontend/src/AppRoutes/pageComponents.ts | 3 +- frontend/src/api/dashboard/update.ts | 4 +- .../variables/dashboardVariablesQuery.ts | 30 ++ frontend/src/api/dashboard/variables/query.ts | 24 - .../ListOfDashboard/DashboardsList.tsx | 378 ++++++++++++++ .../TableComponents/CreatedBy.tsx | 2 +- .../TableComponents/DeleteButton.tsx | 2 +- .../ListOfDashboard/TableComponents/Name.tsx | 2 +- .../ListOfDashboard/TableComponents/Tags.tsx | 2 +- .../src/container/ListOfDashboard/index.tsx | 379 +------------- .../DashboardDescription/index.tsx | 6 +- .../VariableItem/VariableItem.styles.scss | 8 + .../Variables/VariableItem/VariableItem.tsx | 475 ++++++++++-------- .../DashboardSettings/Variables/index.tsx | 14 +- .../NewDashboard/DashboardSettings/index.tsx | 2 +- .../DashboardVariableSelection.styles.scss | 8 + .../DashboardVariableSelection.tsx | 110 ++++ .../VariableItem.test.tsx | 154 +++--- .../VariableItem.tsx | 269 ++++++---- .../DashboardVariablesSelection/index.tsx | 118 +---- .../DashboardVariablesSelection/styles.ts | 23 +- .../TopNav/DateTimeSelection/config.ts | 6 +- .../DashboardsListPage.tsx} | 4 +- .../src/pages/DashboardsListPage/index.tsx | 3 + .../src/pages/NewDashboard/DashboardPage.tsx | 33 ++ frontend/src/pages/NewDashboard/index.tsx | 34 +- .../types/api/dashboard/variables/query.ts | 2 +- frontend/tsconfig.json | 4 +- frontend/webpack.config.js | 2 +- 29 files changed, 1116 insertions(+), 985 deletions(-) create mode 100644 frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts delete mode 100644 frontend/src/api/dashboard/variables/query.ts create mode 100644 frontend/src/container/ListOfDashboard/DashboardsList.tsx create mode 100644 frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.styles.scss create mode 100644 frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss create mode 100644 frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx rename frontend/src/pages/{Dashboard/index.tsx => DashboardsListPage/DashboardsListPage.tsx} (83%) create mode 100644 frontend/src/pages/DashboardsListPage/index.tsx create mode 100644 frontend/src/pages/NewDashboard/DashboardPage.tsx diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index b2892e3d38..638c019506 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -49,7 +49,8 @@ export const Onboarding = Loadable( ); export const DashboardPage = Loadable( - () => import(/* webpackChunkName: "DashboardPage" */ 'pages/Dashboard'), + () => + import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'), ); export const NewDashboardPage = Loadable( diff --git a/frontend/src/api/dashboard/update.ts b/frontend/src/api/dashboard/update.ts index 37341524f8..db5350849e 100644 --- a/frontend/src/api/dashboard/update.ts +++ b/frontend/src/api/dashboard/update.ts @@ -4,7 +4,7 @@ import { AxiosError } from 'axios'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/dashboard/update'; -const update = async ( +const updateDashboard = async ( props: Props, ): Promise | ErrorResponse> => { try { @@ -23,4 +23,4 @@ const update = async ( } }; -export default update; +export default updateDashboard; diff --git a/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts b/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts new file mode 100644 index 0000000000..8605ce75f1 --- /dev/null +++ b/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts @@ -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 | 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; diff --git a/frontend/src/api/dashboard/variables/query.ts b/frontend/src/api/dashboard/variables/query.ts deleted file mode 100644 index 958fdb7e3a..0000000000 --- a/frontend/src/api/dashboard/variables/query.ts +++ /dev/null @@ -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 | 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; diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx new file mode 100644 index 0000000000..8bb56490e8 --- /dev/null +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -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((state) => state.app); + + const [action, createNewDashboard] = useComponentPermission( + ['action', 'create_new_dashboards'], + role, + ); + + const { t } = useTranslation('dashboard'); + + const [ + isImportJSONModalVisible, + setIsImportJSONModalVisible, + ] = useState(false); + + const [uploadedGrafana, setUploadedGrafana] = useState(false); + const [isFilteringDashboards, setIsFilteringDashboards] = useState(false); + + const [dashboards, setDashboards] = useState(); + + 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[] = [ + { + 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[] = [ + { + title: 'Name', + dataIndex: 'name', + width: 40, + render: Name, + }, + { + title: 'Description', + width: 50, + dataIndex: 'description', + }, + { + title: 'Tags', + dataIndex: 'tags', + width: 50, + render: (value): JSX.Element => , + }, + ]; + + 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( + () => ( + + + + + + + + + + + + } + type="primary" + data-testid="create-new-dashboard" + loading={newDashboardState.loading} + danger={newDashboardState.error} + > + {getText()} + + + + + ), + [ + isDashboardListLoading, + handleSearch, + isFilteringDashboards, + getMenuItems, + newDashboardState.loading, + newDashboardState.error, + getText, + ], + ); + + return ( + + {GetHeader} + + + onModalHandler(false)} + /> + + + + ); +} + +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; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx b/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx index d463f80c03..56c5ec8bfb 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx @@ -2,7 +2,7 @@ import { Typography } from 'antd'; import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; import getFormattedDate from 'lib/getFormatedDate'; -import { Data } from '..'; +import { Data } from '../DashboardsList'; function Created(createdBy: Data['createdBy']): JSX.Element { const time = new Date(createdBy); diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index 33663b129d..810b99a278 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -11,7 +11,7 @@ import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; -import { Data } from '..'; +import { Data } from '../DashboardsList'; import { TableLinkText } from './styles'; interface DeleteButtonProps { diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx index a3f3427b6c..deb64ced11 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx @@ -2,7 +2,7 @@ import { LockFilled } from '@ant-design/icons'; import ROUTES from 'constants/routes'; import history from 'lib/history'; -import { Data } from '..'; +import { Data } from '../DashboardsList'; import { TableLinkText } from './styles'; function Name(name: Data['name'], data: Data): JSX.Element { diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx index bc698487d2..761a6bdee6 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/destructuring-assignment */ import { Tag } from 'antd'; -import { Data } from '../index'; +import { Data } from '../DashboardsList'; function Tags(data: Data['tags']): JSX.Element { return ( diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index b9d48aef3c..03cc6ba563 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -1,378 +1,3 @@ -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 DashboardsList from './DashboardsList'; -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 ListOfAllDashboard(): JSX.Element { - const { - data: dashboardListResponse = [], - isLoading: isDashboardListLoading, - refetch: refetchDashboardList, - } = useGetAllDashboard(); - - const { role } = useSelector((state) => state.app); - - const [action, createNewDashboard] = useComponentPermission( - ['action', 'create_new_dashboards'], - role, - ); - - const { t } = useTranslation('dashboard'); - - const [ - isImportJSONModalVisible, - setIsImportJSONModalVisible, - ] = useState(false); - - const [uploadedGrafana, setUploadedGrafana] = useState(false); - const [isFilteringDashboards, setIsFilteringDashboards] = useState(false); - - const [dashboards, setDashboards] = useState(); - - 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[] = [ - { - 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[] = [ - { - title: 'Name', - dataIndex: 'name', - width: 40, - render: Name, - }, - { - title: 'Description', - width: 50, - dataIndex: 'description', - }, - { - title: 'Tags', - dataIndex: 'tags', - width: 50, - render: (value): JSX.Element => , - }, - ]; - - 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( - () => ( - - - - - - - - - - - - } - type="primary" - data-testid="create-new-dashboard" - loading={newDashboardState.loading} - danger={newDashboardState.error} - > - {getText()} - - - - - ), - [ - isDashboardListLoading, - handleSearch, - isFilteringDashboards, - getMenuItems, - newDashboardState.loading, - newDashboardState.error, - getText, - ], - ); - - return ( - - {GetHeader} - - - onModalHandler(false)} - /> - - - - ); -} - -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; +export default DashboardsList; diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 12349cb2c3..beef29497e 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -50,7 +50,7 @@ function DashboardDescription(): JSX.Element { return ( - + )} - + - + {selectedData && ( ([]); - // Internal states - const [previewLoading, setPreviewLoading] = useState(false); // Error messages const [errorName, setErrorName] = useState(false); const [errorPreview, setErrorPreview] = useState(null); @@ -131,232 +124,268 @@ function VariableItem({ }; // Fetches the preview values for the SQL variable query - const handleQueryResult = async (): Promise => { - setPreviewLoading(true); - setErrorPreview(null); - try { - const variableQueryResponse = await query({ - query: variableQueryValue, - variables: variablePropsToPayloadVariables(existingVariables), - }); - 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 handleQueryResult = (response: any): void => { + if (response?.payload?.variableValues) + setPreviewValues( + sortValues( + response.payload?.variableValues || [], + variableSortType, + ) as never, + ); }; + + 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 ( - - {/* Add Variable */} - - - Name - -
- { - setVariableName(e.target.value); - setErrorName( - !validateName(e.target.value) && e.target.value !== variableData.name, - ); - }} - /> +
+
+ {/* Add Variable */} + + + Name +
- - {errorName ? 'Variable name already exists' : ''} - -
-
- - - - Description - - - setVariableDescription(e.target.value)} - /> - - - - Type - - - - - - Options - - {queryType === 'QUERY' && ( - - - Query - - -
- setVariableQueryValue(e)} - height="300px" - /> - -
-
- )} - {queryType === 'CUSTOM' && ( - - - Values separated by comma - - { - setVariableCustomValue(e.target.value); - setPreviewValues( - sortValues( - commaValuesParser(e.target.value), - variableSortType, - ) as never, - ); - }} - /> - - )} - {queryType === 'TEXTBOX' && ( - - - Default Value - - { - setVariableTextboxValue(e.target.value); - }} - placeholder="Default value if any" - style={{ width: 400 }} - /> - - )} - {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( - <> - - - Preview of Values - -
- {errorPreview ? ( - {errorPreview} - ) : ( - map(previewValues, (value, idx) => ( - {value.toString()} - )) - )} -
-
- - - Sort - - - - + value={variableName} + onChange={(e): void => { + setVariableName(e.target.value); + setErrorName( + !validateName(e.target.value) && e.target.value !== variableData.name, + ); + }} + /> +
+ + {errorName ? 'Variable name already exists' : ''} + +
+
+ + + + Description + + + setVariableDescription(e.target.value)} + /> + + + + Type + + + + + + Options + + {queryType === 'QUERY' && ( +
+ + Query + + +
+ setVariableQueryValue(e)} + height="240px" + options={{ + fontSize: 13, + wordWrap: 'on', + lineNumbers: 'off', + glyphMargin: false, + folding: false, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + minimap: { + enabled: false, + }, + }} + /> + +
+
+ )} + {queryType === 'CUSTOM' && ( - Enable multiple values to be checked + Values separated by comma - { - setVariableMultiSelect(e); - if (!e) { - setVariableShowALLOption(false); - } + setVariableCustomValue(e.target.value); + setPreviewValues( + sortValues( + commaValuesParser(e.target.value), + variableSortType, + ) as never, + ); }} /> - {variableMultiSelect && ( + )} + {queryType === 'TEXTBOX' && ( + + + Default Value + + { + setVariableTextboxValue(e.target.value); + }} + placeholder="Default value if any" + style={{ width: 400 }} + /> + + )} + {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( + <> - Include an option for ALL values + Preview of Values + +
+ {errorPreview ? ( + {errorPreview} + ) : ( + map(previewValues, (value, idx) => ( + {value.toString()} + )) + )} +
+
+ + + Sort + + + + + + + Enable multiple values to be checked setVariableShowALLOption(e)} + checked={variableMultiSelect} + onChange={(e): void => { + setVariableMultiSelect(e); + if (!e) { + setVariableShowALLOption(false); + } + }} /> - )} - - )} - - - - - - + {variableMultiSelect && ( + + + Include an option for ALL values + + setVariableShowALLOption(e)} + /> + + )} + + )} +
+ +
+ + + + + +
+ ); } diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index 754e44f1bc..de23e64068 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -4,6 +4,7 @@ import { Button, Modal, Row, Space, Tag } from 'antd'; import { ResizeTable } from 'components/ResizeTable'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; +import { PencilIcon, TrashIcon } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -134,7 +135,7 @@ function VariablesSetting(): JSX.Element { key: 'name', }, { - title: 'Definition', + title: 'Description', dataIndex: 'description', width: 100, key: 'description', @@ -147,19 +148,19 @@ function VariablesSetting(): JSX.Element { ), @@ -187,9 +188,10 @@ function VariablesSetting(): JSX.Element { onVariableViewModeEnter('ADD', {} as IDashboardVariable) } > - New Variables + Add Variable
+ )} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx index 4cbc531c9d..dafa0f8789 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx @@ -13,7 +13,7 @@ function DashboardSettingsContent(): JSX.Element { { label: 'Variables', key: 'variables', children: }, ]; - return ; + return ; } export default DashboardSettingsContent; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss new file mode 100644 index 0000000000..1d91614ff6 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss @@ -0,0 +1,8 @@ +.variable-name { + font-size: 0.8rem; + min-width: 100px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: gray; +} diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx new file mode 100644 index 0000000000..647f72dbb0 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx @@ -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(false); + const [lastUpdatedVar, setLastUpdatedVar] = useState(''); + + const { role } = useSelector((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 ( + + {variablesKeys && + map(variablesKeys, (variableName) => ( + + ))} + + ); +} + +export default memo(DashboardVariableSelection); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx index 7543821b60..f3a2c0e4d0 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx @@ -1,6 +1,13 @@ 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 { IDashboardVariable } from 'types/api/dashboard/getAll'; @@ -25,7 +32,6 @@ const mockCustomVariableData: IDashboardVariable = { }; const mockOnValueUpdate = jest.fn(); -const mockOnAllSelectedUpdate = jest.fn(); describe('VariableItem', () => { let useEffectSpy: jest.SpyInstance; @@ -41,13 +47,14 @@ describe('VariableItem', () => { test('renders component with default props', () => { render( - , + + + , ); expect(screen.getByText('$testVariable')).toBeInTheDocument(); @@ -55,45 +62,55 @@ describe('VariableItem', () => { test('renders Input when the variable type is TEXTBOX', () => { render( - , + + + , ); 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( - , + + + , ); - const inputElement = screen.getByPlaceholderText('Enter value'); - fireEvent.change(inputElement, { target: { value: 'newValue' } }); - expect(mockOnValueUpdate).toHaveBeenCalledTimes(1); - expect(mockOnValueUpdate).toHaveBeenCalledWith('testVariable', 'newValue'); - expect(mockOnAllSelectedUpdate).toHaveBeenCalledTimes(1); - expect(mockOnAllSelectedUpdate).toHaveBeenCalledWith('testVariable', false); + act(() => { + const inputElement = screen.getByPlaceholderText('Enter value'); + fireEvent.change(inputElement, { target: { value: 'newValue' } }); + }); + + await waitFor(() => { + // expect(mockOnValueUpdate).toHaveBeenCalledTimes(1); + expect(mockOnValueUpdate).toHaveBeenCalledWith( + 'testVariable', + 'newValue', + false, + ); + }); }); test('renders a Select element when variable type is CUSTOM', () => { render( - , + + + , ); expect(screen.getByText('$customVariable')).toBeInTheDocument(); @@ -107,13 +124,14 @@ describe('VariableItem', () => { }; render( - , + + + , ); expect(screen.getByTitle('ALL')).toBeInTheDocument(); @@ -121,48 +139,16 @@ describe('VariableItem', () => { test('calls useEffect when the component mounts', () => { render( - , + + + , ); expect(useEffect).toHaveBeenCalled(); }); - - test('calls useEffect only once when the component mounts', () => { - // Render the component - const { rerender } = render( - , - ); - - // Create an updated version of the mock data - const updatedMockCustomVariableData = { - ...mockCustomVariableData, - selectedValue: 'option1', - }; - - // Re-render the component with the updated data - rerender( - , - ); - - // Check if the useEffect is called with the correct arguments - expect(useEffectSpy).toHaveBeenCalledTimes(4); - }); }); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index 2a46d57f8e..4e81b0b116 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -1,27 +1,35 @@ +import './DashboardVariableSelection.styles.scss'; + import { orange } from '@ant-design/colors'; import { WarningOutlined } from '@ant-design/icons'; 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 sortValues from 'lib/dashbaordVariables/sortVariableValues'; 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 { VariableResponseProps } from 'types/api/dashboard/variables/query'; import { variablePropsToPayloadVariables } from '../utils'; -import { SelectItemStyle, VariableContainer, VariableName } from './styles'; +import { SelectItemStyle, VariableContainer, VariableValue } from './styles'; import { areArraysEqual } from './util'; const ALL_SELECT_VALUE = '__ALL__'; +const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g; + interface VariableItemProps { variableData: IDashboardVariable; existingVariables: Record; onValueUpdate: ( name: string, arg1: IDashboardVariable['selectedValue'], + allSelected: boolean, ) => void; - onAllSelectedUpdate: (name: string, arg1: boolean) => void; lastUpdatedVar: string; } @@ -38,48 +46,74 @@ function VariableItem({ variableData, existingVariables, onValueUpdate, - onAllSelectedUpdate, lastUpdatedVar, }: VariableItemProps): JSX.Element { const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( [], ); - const [isLoading, setIsLoading] = useState(false); + + const [variableValue, setVaribleValue] = useState( + variableData?.selectedValue?.toString() || '', + ); + + const debouncedVariableValue = useDebounce(variableValue, 500); const [errorMessage, setErrorMessage] = useState(null); - /* eslint-disable sonarjs/cognitive-complexity */ - const getOptions = useCallback(async (): Promise => { - if (variableData.type === 'QUERY') { + useEffect(() => { + const { selectedValue } = variableData; + + 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 { setErrorMessage(null); - setIsLoading(true); - const response = await query({ - query: variableData.queryValue || '', - variables: variablePropsToPayloadVariables(existingVariables), - }); - - 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) { + if ( + variablesRes?.variableValues && + Array.isArray(variablesRes?.variableValues) + ) { const newOptionsData = sortValues( - response.payload?.variableValues, + variablesRes?.variableValues, 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; + if (!areArraysEqual(newOptionsData, oldOptionsData)) { /* eslint-disable no-useless-escape */ const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}` @@ -104,10 +138,10 @@ function VariableItem({ [value] = newOptionsData; } if (variableData.name) { - onValueUpdate(variableData.name, value); - onAllSelectedUpdate(variableData.name, allSelected); + onValueUpdate(variableData.name, value, allSelected); } } + setOptionsData(newOptionsData); } } @@ -122,19 +156,37 @@ function VariableItem({ ) as never, ); } - }, [ - variableData, - existingVariables, - onValueUpdate, - onAllSelectedUpdate, - optionsData, - lastUpdatedVar, - ]); - - useEffect(() => { - getOptions(); // 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 => { if (variableData.name) @@ -143,11 +195,9 @@ function VariableItem({ (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || (Array.isArray(value) && value.length === 0) ) { - onValueUpdate(variableData.name, optionsData); - onAllSelectedUpdate(variableData.name, true); + onValueUpdate(variableData.name, optionsData, true); } else { - onValueUpdate(variableData.name, value); - onAllSelectedUpdate(variableData.name, false); + onValueUpdate(variableData.name, value, false); } }; @@ -165,61 +215,78 @@ function VariableItem({ ? 'multiple' : undefined; const enableSelectAll = variableData.multiSelect && variableData.showALLOption; + + useEffect(() => { + if (debouncedVariableValue !== variableData?.selectedValue?.toString()) { + handleChange(debouncedVariableValue); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedVariableValue]); + return ( - ${variableData.name} - {variableData.type === 'TEXTBOX' ? ( - { - handleChange(e.target.value || ''); - }} - style={{ - width: - 50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50), - }} - /> - ) : ( - !errorMessage && ( - - {enableSelectAll && ( - - ALL - - )} - {map(optionsData, (option) => ( - - {option.toString()} - - ))} - - ) - )} - {errorMessage && ( - - {errorMessage}}> - - - - )} + value={variableValue} + onChange={(e): void => { + setVaribleValue(e.target.value || ''); + }} + style={{ + width: + 50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50), + }} + /> + ) : ( + !errorMessage && + optionsData && ( + + ) + )} + {errorMessage && ( + + {errorMessage}} + > + + + + )} + ); } diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx index 439ab98a67..5b8e9e48c6 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -1,117 +1,3 @@ -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 DashboardVariableSelection from './DashboardVariableSelection'; -import VariableItem from './VariableItem'; - -function DashboardVariableSelection(): JSX.Element | null { - const { selectedDashboard, setSelectedDashboard } = useDashboard(); - - const { data } = selectedDashboard || {}; - - const { variables } = data || {}; - - const [update, setUpdate] = useState(false); - const [lastUpdatedVar, setLastUpdatedVar] = useState(''); - - const { role } = useSelector((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 ( - - {map(sortBy(Object.keys(variables)), (variableName) => ( - - ))} - - ); -} - -export default memo(DashboardVariableSelection); +export default DashboardVariableSelection; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts index 76ba50c38c..5c5de3e97e 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts @@ -3,19 +3,40 @@ import { Typography } from 'antd'; import styled from 'styled-components'; export const VariableContainer = styled.div` + max-width: 100%; border: 1px solid ${grey[1]}66; border-radius: 2px; padding: 0; padding-left: 0.5rem; + margin-right: 8px; display: flex; align-items: center; margin-bottom: 0.3rem; + gap: 4px; + padding: 4px; `; export const VariableName = styled(Typography)` font-size: 0.8rem; - font-style: italic; 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 = { diff --git a/frontend/src/container/TopNav/DateTimeSelection/config.ts b/frontend/src/container/TopNav/DateTimeSelection/config.ts index 7b51837cce..cada5a3194 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelection/config.ts @@ -68,11 +68,7 @@ export const getOptions = (routes: string): Option[] => { return Options; }; -export const routesToHideBreadCrumbs = [ - ROUTES.SUPPORT, - ROUTES.ALL_DASHBOARD, - ROUTES.DASHBOARD, -]; +export const routesToHideBreadCrumbs = [ROUTES.SUPPORT, ROUTES.ALL_DASHBOARD]; export const routesToSkip = [ ROUTES.SETTINGS, diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx similarity index 83% rename from frontend/src/pages/Dashboard/index.tsx rename to frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx index 35b801387a..415c081a27 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx @@ -3,7 +3,7 @@ import ReleaseNote from 'components/ReleaseNote'; import ListOfAllDashboard from 'container/ListOfDashboard'; import { useLocation } from 'react-router-dom'; -function Dashboard(): JSX.Element { +function DashboardsListPage(): JSX.Element { const location = useLocation(); return ( @@ -14,4 +14,4 @@ function Dashboard(): JSX.Element { ); } -export default Dashboard; +export default DashboardsListPage; diff --git a/frontend/src/pages/DashboardsListPage/index.tsx b/frontend/src/pages/DashboardsListPage/index.tsx new file mode 100644 index 0000000000..e0007017e4 --- /dev/null +++ b/frontend/src/pages/DashboardsListPage/index.tsx @@ -0,0 +1,3 @@ +import DashboardsListPage from './DashboardsListPage'; + +export default DashboardsListPage; diff --git a/frontend/src/pages/NewDashboard/DashboardPage.tsx b/frontend/src/pages/NewDashboard/DashboardPage.tsx new file mode 100644 index 0000000000..d880413628 --- /dev/null +++ b/frontend/src/pages/NewDashboard/DashboardPage.tsx @@ -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 ; + } + + if (isError && errorMessage) { + return {errorMessage}; + } + + if (isLoading) { + return ; + } + + return ; +} + +export default DashboardPage; diff --git a/frontend/src/pages/NewDashboard/index.tsx b/frontend/src/pages/NewDashboard/index.tsx index 105d21cfdf..1b30cb0919 100644 --- a/frontend/src/pages/NewDashboard/index.tsx +++ b/frontend/src/pages/NewDashboard/index.tsx @@ -1,33 +1,3 @@ -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'; +import DashboardPage from './DashboardPage'; -function NewDashboardPage(): 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 ; - } - - if (isError && errorMessage) { - return {errorMessage}; - } - - if (isLoading) { - return ; - } - - return ; -} - -export default NewDashboardPage; +export default DashboardPage; diff --git a/frontend/src/types/api/dashboard/variables/query.ts b/frontend/src/types/api/dashboard/variables/query.ts index c535ad72be..6f4589a413 100644 --- a/frontend/src/types/api/dashboard/variables/query.ts +++ b/frontend/src/types/api/dashboard/variables/query.ts @@ -10,6 +10,6 @@ export type Props = { variables: PayloadVariables; }; -export type PayloadProps = { +export type VariableResponseProps = { variableValues: string[] | number[]; }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index bc93c222d9..a81ac69961 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "sourceMap": true, "outDir": "./dist/", "noImplicitAny": true, "module": "esnext", @@ -20,11 +21,12 @@ "baseUrl": "./src", "downlevelIteration": true, "plugins": [{ "name": "typescript-plugin-css-modules" }], - "types": ["node", "jest"] + "types": ["node", "jest"], }, "exclude": ["node_modules"], "include": [ "./src", + "./src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "./babel.config.js", "./jest.config.ts", "./.eslintrc.js", diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index a5862179a4..58635d8994 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -46,7 +46,7 @@ if (process.env.BUNDLE_ANALYSER === 'true') { */ const config = { mode: 'development', - devtool: 'source-map', + devtool: 'eval-source-map', entry: resolve(__dirname, './src/index.tsx'), devServer: { historyApiFallback: true,