diff --git a/frontend/src/api/dashboard/variables/query.ts b/frontend/src/api/dashboard/variables/query.ts new file mode 100644 index 0000000000..61693ba4c0 --- /dev/null +++ b/frontend/src/api/dashboard/variables/query.ts @@ -0,0 +1,26 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/dashboard/variables/query'; + +const query = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/variables/query?query=${encodeURIComponent(props.query)}`, + ); + + 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/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx index 8fe3b259c1..113049a295 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx @@ -7,6 +7,7 @@ import { timeItems, timePreferance, } from 'container/NewWidget/RightContainer/timeItems'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import getChartData from 'lib/getChartData'; import React, { useCallback, useState } from 'react'; import { useQuery } from 'react-query'; @@ -52,6 +53,7 @@ function FullView({ graphType: widget.panelTypes, query: widget.query, globalSelectedInterval: globalSelectedTime, + variables: getDashboardVariables(), }), ); diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index f39d64cb76..4d654a0222 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -3,6 +3,7 @@ import { AxiosError } from 'axios'; import { ChartData } from 'chart.js'; import Spinner from 'components/Spinner'; import GridGraphComponent from 'container/GridGraphComponent'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import getChartData from 'lib/getChartData'; import isEmpty from 'lodash-es/isEmpty'; import React, { memo, useCallback, useEffect, useState } from 'react'; @@ -104,11 +105,18 @@ function GridCardGraph({ useEffect(() => { (async (): Promise => { try { + setState((state) => ({ + ...state, + error: false, + errorMessage: '', + loading: true, + })); const response = await GetMetricQueryRange({ selectedTime: widget.timePreferance, graphType: widget.panelTypes, query: widget.query, globalSelectedInterval, + variables: getDashboardVariables(), }); const isError = response.error; @@ -144,6 +152,11 @@ function GridCardGraph({ errorMessage: (error as AxiosError).toString(), loading: false, })); + } finally { + setState((state) => ({ + ...state, + loading: false, + })); } })(); }, [widget, maxTime, minTime, globalSelectedInterval]); diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx index d80b12f7a1..234b10ceb7 100644 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -121,6 +121,7 @@ function GridGraph(props: Props): JSX.Element { name: data.name, tags: data.tags, widgets: data.widgets, + variables: data.variables, layout, }, uuid: selectedDashboard.uuid, @@ -157,6 +158,7 @@ function GridGraph(props: Props): JSX.Element { data.name, data.tags, data.title, + data.variables, data.widgets, dispatch, saveLayoutPermission, diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts index 0d7320f6f5..d06c69c757 100644 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ b/frontend/src/container/GridGraphLayout/utils.ts @@ -27,6 +27,7 @@ export const UpdateDashboard = async ({ description: data.description, name: data.name, tags: data.tags, + variables: data.variables, widgets: [ ...(data.widgets || []), { diff --git a/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts b/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts index db9825b677..369a0dffa3 100644 --- a/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts +++ b/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts @@ -12,6 +12,7 @@ describe('executeSearchQueries', () => { updated_at: '', data: { title: 'first dashboard', + variables: {}, }, }; const secondDashboard: Dashboard = { @@ -21,6 +22,7 @@ describe('executeSearchQueries', () => { updated_at: '', data: { title: 'second dashboard', + variables: {}, }, }; const thirdDashboard: Dashboard = { @@ -30,6 +32,7 @@ describe('executeSearchQueries', () => { updated_at: '', data: { title: 'third dashboard (with special characters +?\\)', + variables: {}, }, }; const dashboards = [firstDashboard, secondDashboard, thirdDashboard]; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/index.tsx rename to frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/styles.ts similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/styles.ts rename to frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/styles.ts diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/Description/index.tsx similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/index.tsx rename to frontend/src/container/NewDashboard/DashboardSettings/General/Description/index.tsx diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/General/Description/styles.ts similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/styles.ts rename to frontend/src/container/NewDashboard/DashboardSettings/General/Description/styles.ts diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx new file mode 100644 index 0000000000..4a8fce57a2 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx @@ -0,0 +1,112 @@ +import { SaveOutlined } from '@ant-design/icons'; +import { Col, Divider, Input, Space, Typography } from 'antd'; +import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; +import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { + UpdateDashboardTitleDescriptionTags, + UpdateDashboardTitleDescriptionTagsProps, +} from 'store/actions'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import DashboardReducer from 'types/reducer/dashboards'; + +import { Button } from './styles'; + +function GeneralDashboardSettings({ + updateDashboardTitleDescriptionTags, +}: DescriptionOfDashboardProps): JSX.Element { + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + + const [selectedDashboard] = dashboards; + const selectedData = selectedDashboard.data; + const { title } = selectedData; + const { tags } = selectedData; + const { description } = selectedData; + + const [updatedTitle, setUpdatedTitle] = useState(title); + const [updatedTags, setUpdatedTags] = useState(tags || []); + const [updatedDescription, setUpdatedDescription] = useState( + description || '', + ); + + const { t } = useTranslation('common'); + + const onSaveHandler = useCallback(() => { + const dashboard = selectedDashboard; + // @TODO need to update this function to take title,description,tags only + updateDashboardTitleDescriptionTags({ + dashboard: { + ...dashboard, + data: { + ...dashboard.data, + description: updatedDescription, + tags: updatedTags, + title: updatedTitle, + }, + }, + }); + }, [ + updatedTitle, + updatedTags, + updatedDescription, + selectedDashboard, + updateDashboardTitleDescriptionTags, + ]); + + return ( + + +
+ Name + setUpdatedTitle(e.target.value)} + /> +
+ +
+ Description + setUpdatedDescription(e.target.value)} + /> +
+
+ Tags + +
+
+ + +
+
+ + ); +} + +interface DispatchProps { + updateDashboardTitleDescriptionTags: ( + props: UpdateDashboardTitleDescriptionTagsProps, + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateDashboardTitleDescriptionTags: bindActionCreators( + UpdateDashboardTitleDescriptionTags, + dispatch, + ), +}); + +type DescriptionOfDashboardProps = DispatchProps; + +export default connect(null, mapDispatchToProps)(GeneralDashboardSettings); diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/General/styles.ts new file mode 100644 index 0000000000..43bcccef51 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/styles.ts @@ -0,0 +1,20 @@ +import { Button as ButtonComponent, Drawer } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled.div` + margin-top: 0.5rem; +`; + +export const Button = styled(ButtonComponent)` + &&& { + display: flex; + align-items: center; + } +`; + +export const DrawerContainer = styled(Drawer)` + .ant-drawer-header { + padding: 0; + border: none; + } +`; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx new file mode 100644 index 0000000000..31d443abff --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx @@ -0,0 +1,349 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import { orange } from '@ant-design/colors'; +import { + Button, + Col, + Divider, + Input, + Select, + Switch, + Tag, + Typography, +} from 'antd'; +import query from 'api/dashboard/variables/query'; +import Editor from 'components/Editor'; +import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; +import sortValues from 'lib/dashbaordVariables/sortVariableValues'; +import { map } from 'lodash-es'; +import React, { useEffect, useState } from 'react'; +import { + IDashboardVariable, + TSortVariableValuesType, + TVariableQueryType, + VariableQueryTypeArr, + VariableSortTypeArr, +} from 'types/api/dashboard/getAll'; +import { v4 } from 'uuid'; + +import { TVariableViewMode } from '../types'; +import { LabelContainer, VariableItemRow } from './styles'; + +const { Option } = Select; + +interface VariableItemProps { + variableData: IDashboardVariable; + onCancel: () => void; + onSave: (name: string, arg0: IDashboardVariable) => void; + validateName: (arg0: string) => boolean; + variableViewMode: TVariableViewMode; +} +function VariableItem({ + variableData, + onCancel, + onSave, + validateName, + variableViewMode, +}: VariableItemProps): JSX.Element { + const [variableName, setVariableName] = useState( + variableData.name || '', + ); + const [variableDescription, setVariableDescription] = useState( + variableData.description || '', + ); + const [queryType, setQueryType] = useState( + variableData.type || 'QUERY', + ); + const [variableQueryValue, setVariableQueryValue] = useState( + variableData.queryValue || '', + ); + const [variableCustomValue, setVariableCustomValue] = useState( + variableData.customValue || '', + ); + const [variableTextboxValue, setVariableTextboxValue] = useState( + variableData.textboxValue || '', + ); + const [ + variableSortType, + setVariableSortType, + ] = useState( + variableData.sort || VariableSortTypeArr[0], + ); + const [variableMultiSelect, setVariableMultiSelect] = useState( + variableData.multiSelect || false, + ); + const [variableShowALLOption, setVariableShowALLOption] = useState( + variableData.showALLOption || false, + ); + const [previewValues, setPreviewValues] = useState([]); + + // Internal states + const [previewLoading, setPreviewLoading] = useState(false); + // Error messages + const [errorName, setErrorName] = useState(false); + const [errorPreview, setErrorPreview] = useState(null); + + useEffect(() => { + setPreviewValues([]); + if (queryType === 'CUSTOM') { + setPreviewValues( + sortValues( + commaValuesParser(variableCustomValue), + variableSortType, + ) as never, + ); + } + }, [ + queryType, + variableCustomValue, + variableData.customValue, + variableData.type, + variableSortType, + ]); + + const handleSave = (): void => { + const newVariableData: IDashboardVariable = { + name: variableName, + description: variableDescription, + type: queryType, + queryValue: variableQueryValue, + customValue: variableCustomValue, + textboxValue: variableTextboxValue, + multiSelect: variableMultiSelect, + showALLOption: variableShowALLOption, + sort: variableSortType, + ...(queryType === 'TEXTBOX' && { + selectedValue: (variableData.selectedValue || + variableTextboxValue) as never, + }), + modificationUUID: v4(), + }; + onSave( + (variableViewMode === 'EDIT' ? variableData.name : variableName) as string, + newVariableData, + ); + onCancel(); + }; + + // Fetches the preview values for the SQL variable query + const handleQueryResult = async (): Promise => { + setPreviewLoading(true); + setErrorPreview(null); + try { + const variableQueryResponse = await query({ + query: variableQueryValue, + }); + setPreviewLoading(false); + if (variableQueryResponse.error) { + setErrorPreview(variableQueryResponse.error); + return; + } + if (variableQueryResponse.payload?.variableValues) + setPreviewValues( + sortValues( + variableQueryResponse.payload?.variableValues || [], + variableSortType, + ) as never, + ); + } catch (e) { + console.error(e); + } + }; + return ( + + {/* Add Variable */} + + + Name + +
+ { + setVariableName(e.target.value); + setErrorName(!validateName(e.target.value)); + }} + /> +
+ + {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 + + + + + + + Enable multiple values to be checked + + { + setVariableMultiSelect(e); + if (!e) { + setVariableShowALLOption(false); + } + }} + /> + + {variableMultiSelect && ( + + + Include an option for ALL values + + setVariableShowALLOption(e)} + /> + + )} + + )} + + + + + + + ); +} + +export default VariableItem; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/styles.ts new file mode 100644 index 0000000000..2d603ede18 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/styles.ts @@ -0,0 +1,11 @@ +import { Row } from 'antd'; +import styled from 'styled-components'; + +export const VariableItemRow = styled(Row)` + gap: 1rem; + margin-bottom: 1rem; +`; + +export const LabelContainer = styled.div` + width: 200px; +`; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx new file mode 100644 index 0000000000..f070752ac4 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -0,0 +1,188 @@ +import { blue, red } from '@ant-design/colors'; +import { PlusOutlined } from '@ant-design/icons'; +import { Button, Modal, Row, Space, Table, Tag } from 'antd'; +import React, { useRef, useState } from 'react'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import DashboardReducer from 'types/reducer/dashboards'; + +import { TVariableViewMode } from './types'; +import VariableItem from './VariableItem/VariableItem'; + +function VariablesSetting({ + updateDashboardVariables, +}: DispatchProps): JSX.Element { + const variableToDelete = useRef(null); + const [deleteVariableModal, setDeleteVariableModal] = useState(false); + + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + + const [selectedDashboard] = dashboards; + + const { + data: { variables = {} }, + } = selectedDashboard; + + const variablesTableData = Object.keys(variables).map((variableName) => ({ + key: variableName, + name: variableName, + ...variables[variableName], + })); + + const [ + variableViewMode, + setVariableViewMode, + ] = useState(null); + + const [ + variableEditData, + setVariableEditData, + ] = useState(null); + + const onDoneVariableViewMode = (): void => { + setVariableViewMode(null); + setVariableEditData(null); + }; + + const onVariableViewModeEnter = ( + viewType: TVariableViewMode, + varData: IDashboardVariable, + ): void => { + setVariableEditData(varData); + setVariableViewMode(viewType); + }; + + const onVariableSaveHandler = ( + name: string, + variableData: IDashboardVariable, + ): void => { + if (!variableData.name) { + return; + } + const newVariables = { ...variables }; + newVariables[variableData.name] = variableData; + if (variableViewMode === 'EDIT') delete newVariables[name]; + updateDashboardVariables(newVariables); + onDoneVariableViewMode(); + }; + + const onVariableDeleteHandler = (variableName: string): void => { + variableToDelete.current = variableName; + setDeleteVariableModal(true); + }; + + const handleDeleteConfirm = (): void => { + const newVariables = { ...variables }; + if (variableToDelete?.current) delete newVariables[variableToDelete?.current]; + updateDashboardVariables(newVariables); + variableToDelete.current = null; + setDeleteVariableModal(false); + }; + const handleDeleteCancel = (): void => { + variableToDelete.current = null; + setDeleteVariableModal(false); + }; + + const validateVariableName = (name: string): boolean => { + return !variables[name]; + }; + + const columns = [ + { + title: 'Variable', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Definition', + dataIndex: 'description', + key: 'description', + }, + { + title: 'Actions', + key: 'action', + render: (_: IDashboardVariable): JSX.Element => ( + + + + + ), + }, + ]; + + return ( + <> + {variableViewMode ? ( + + ) : ( + <> + + + + + + )} + + Are you sure you want to delete variable{' '} + {variableToDelete.current}? + + + ); +} + +interface DispatchProps { + updateDashboardVariables: ( + props: Record, + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateDashboardVariables: bindActionCreators( + UpdateDashboardVariables, + dispatch, + ), +}); + +export default connect(null, mapDispatchToProps)(VariablesSetting); diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/types.ts b/frontend/src/container/NewDashboard/DashboardSettings/Variables/types.ts new file mode 100644 index 0000000000..877372f653 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/types.ts @@ -0,0 +1 @@ +export type TVariableViewMode = 'EDIT' | 'ADD'; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx new file mode 100644 index 0000000000..d8cfa57ff6 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx @@ -0,0 +1,22 @@ +import { Tabs } from 'antd'; +import React from 'react'; + +import GeneralDashboardSettings from './General'; +import VariablesSetting from './Variables'; + +const { TabPane } = Tabs; + +function DashboardSettingsContent(): JSX.Element { + return ( + + + + + + + + + ); +} + +export default DashboardSettingsContent; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx new file mode 100644 index 0000000000..b40d113be4 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -0,0 +1,137 @@ +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 { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; +import sortValues from 'lib/dashbaordVariables/sortVariableValues'; +import { map } from 'lodash-es'; +import React, { useCallback, useEffect, useState } from 'react'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; + +import { VariableContainer, VariableName } from './styles'; + +const { Option } = Select; + +const ALL_SELECT_VALUE = '__ALL__'; + +interface VariableItemProps { + variableData: IDashboardVariable; + onValueUpdate: (name: string | undefined, arg1: string | string[]) => void; + onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void; +} +function VariableItem({ + variableData, + onValueUpdate, + onAllSelectedUpdate, +}: VariableItemProps): JSX.Element { + const [optionsData, setOptionsData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const [errorMessage, setErrorMessage] = useState(null); + const getOptions = useCallback(async (): Promise => { + if (variableData.type === 'QUERY') { + try { + setErrorMessage(null); + setIsLoading(true); + + const response = await query({ + query: variableData.queryValue || '', + }); + + setIsLoading(false); + if (response.error) { + setErrorMessage(response.error); + return; + } + if (response.payload?.variableValues) + setOptionsData( + sortValues(response.payload?.variableValues, variableData.sort) as never, + ); + } catch (e) { + console.error(e); + } + } else if (variableData.type === 'CUSTOM') { + setOptionsData( + sortValues( + commaValuesParser(variableData.customValue || ''), + variableData.sort, + ) as never, + ); + } + }, [ + variableData.customValue, + variableData.queryValue, + variableData.sort, + variableData.type, + ]); + + useEffect(() => { + getOptions(); + }, [getOptions]); + + const handleChange = (value: string | string[]): void => { + if ( + value === ALL_SELECT_VALUE || + (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) + ) { + onValueUpdate(variableData.name, optionsData); + onAllSelectedUpdate(variableData.name, true); + } else { + onValueUpdate(variableData.name, value); + onAllSelectedUpdate(variableData.name, false); + } + }; + return ( + + ${variableData.name} + {variableData.type === 'TEXTBOX' ? ( + { + handleChange(e.target.value || ''); + }} + style={{ + width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50), + }} + /> + ) : ( + + )} + {errorMessage && ( + + {errorMessage}}> + + + + )} + + ); +} + +export default VariableItem; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx new file mode 100644 index 0000000000..c3aad577bf --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -0,0 +1,72 @@ +import { Row } from 'antd'; +import { map, sortBy } from 'lodash-es'; +import React from 'react'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import DashboardReducer from 'types/reducer/dashboards'; + +import VariableItem from './VariableItem'; + +function DashboardVariableSelection({ + updateDashboardVariables, +}: DispatchProps): JSX.Element { + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + const [selectedDashboard] = dashboards; + const { + data: { variables = {} }, + } = selectedDashboard; + + const onValueUpdate = ( + name: string, + value: IDashboardVariable['selectedValue'], + ): void => { + const updatedVariablesData = { ...variables }; + updatedVariablesData[name].selectedValue = value; + updateDashboardVariables(updatedVariablesData); + }; + const onAllSelectedUpdate = ( + name: string, + value: IDashboardVariable['allSelected'], + ): void => { + const updatedVariablesData = { ...variables }; + updatedVariablesData[name].allSelected = value; + updateDashboardVariables(updatedVariablesData); + }; + + return ( + + {map(sortBy(Object.keys(variables)), (variableName) => ( + + ))} + + ); +} + +interface DispatchProps { + updateDashboardVariables: ( + props: Parameters[0], + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateDashboardVariables: bindActionCreators( + UpdateDashboardVariables, + dispatch, + ), +}); + +export default connect(null, mapDispatchToProps)(DashboardVariableSelection); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts new file mode 100644 index 0000000000..9b4b1d3322 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts @@ -0,0 +1,19 @@ +import { grey } from '@ant-design/colors'; +import { Typography } from 'antd'; +import styled from 'styled-components'; + +export const VariableContainer = styled.div` + border: 1px solid ${grey[1]}66; + border-radius: 2px; + padding: 0; + padding-left: 0.5rem; + display: flex; + align-items: center; + margin-bottom: 0.3rem; +`; + +export const VariableName = styled(Typography)` + font-size: 0.8rem; + font-style: italic; + color: ${grey[0]}; +`; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx new file mode 100644 index 0000000000..b8f6617204 --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx @@ -0,0 +1,37 @@ +import { SettingOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import React, { useState } from 'react'; + +import DashboardSettingsContent from '../DashboardSettings'; +import { DrawerContainer } from './styles'; + +function SettingsDrawer(): JSX.Element { + const [visible, setVisible] = useState(false); // TODO Make it False + + const showDrawer = (): void => { + setVisible(true); + }; + + const onClose = (): void => { + setVisible(false); + }; + + return ( + <> + + + + + + ); +} + +export default SettingsDrawer; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx index bcaa553e62..c1a98cce8f 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx @@ -1,135 +1,69 @@ -import { - EditOutlined, - SaveOutlined, - ShareAltOutlined, -} from '@ant-design/icons'; -import { Card, Col, Row, Space, Tag, Typography } from 'antd'; -import AddTags from 'container/NewDashboard/DescriptionOfDashboard/AddTags'; -import NameOfTheDashboard from 'container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard'; +import { ShareAltOutlined } from '@ant-design/icons'; +import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd'; import useComponentPermission from 'hooks/useComponentPermission'; -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - ToggleEditMode, - UpdateDashboardTitleDescriptionTags, - UpdateDashboardTitleDescriptionTagsProps, -} from 'store/actions'; +import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; import AppReducer from 'types/reducer/app'; import DashboardReducer from 'types/reducer/dashboards'; -import Description from './Description'; +import DashboardVariableSelection from '../DashboardVariablesSelection'; +import SettingsDrawer from './SettingsDrawer'; import ShareModal from './ShareModal'; -import { Button, Container } from './styles'; -function DescriptionOfDashboard({ - updateDashboardTitleDescriptionTags, - toggleEditMode, -}: DescriptionOfDashboardProps): JSX.Element { - const { dashboards, isEditMode } = useSelector( +function DescriptionOfDashboard(): JSX.Element { + const { dashboards } = useSelector( (state) => state.dashboards, ); const [selectedDashboard] = dashboards; const selectedData = selectedDashboard.data; - const { title } = selectedData; - const { tags } = selectedData; - const { description } = selectedData; + const { title, tags, description } = selectedData; - const [updatedTitle, setUpdatedTitle] = useState(title); - const [updatedTags, setUpdatedTags] = useState(tags || []); - const [updatedDescription, setUpdatedDescription] = useState( - description || '', - ); const [isJSONModalVisible, isIsJSONModalVisible] = useState(false); const { t } = useTranslation('common'); const { role } = useSelector((state) => state.app); const [editDashboard] = useComponentPermission(['edit_dashboard'], role); - const onClickEditHandler = useCallback(() => { - if (isEditMode) { - const dashboard = selectedDashboard; - // @TODO need to update this function to take title,description,tags only - updateDashboardTitleDescriptionTags({ - dashboard: { - ...dashboard, - data: { - ...dashboard.data, - description: updatedDescription, - tags: updatedTags, - title: updatedTitle, - }, - }, - }); - } else { - toggleEditMode(); - } - }, [ - isEditMode, - updatedTitle, - updatedTags, - updatedDescription, - selectedDashboard, - toggleEditMode, - updateDashboardTitleDescriptionTags, - ]); - const onToggleHandler = (): void => { isIsJSONModalVisible((state) => !state); }; return ( - - {!isEditMode ? ( - - {title} - - {tags?.map((e) => ( - {e} - ))} - - - {description} - - - ) : ( - - - - - - )} - - - + + + + {title} + + {description} +
+ {tags?.map((e) => ( + {e} + ))} +
+ + + - - {editDashboard && ( - - )} @@ -137,23 +71,4 @@ function DescriptionOfDashboard({ ); } -interface DispatchProps { - updateDashboardTitleDescriptionTags: ( - props: UpdateDashboardTitleDescriptionTagsProps, - ) => (dispatch: Dispatch) => void; - toggleEditMode: () => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - updateDashboardTitleDescriptionTags: bindActionCreators( - UpdateDashboardTitleDescriptionTags, - dispatch, - ), - toggleEditMode: bindActionCreators(ToggleEditMode, dispatch), -}); - -type DescriptionOfDashboardProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(DescriptionOfDashboard); +export default DescriptionOfDashboard; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts b/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts index 74794e163c..43bcccef51 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts @@ -1,4 +1,4 @@ -import { Button as ButtonComponent } from 'antd'; +import { Button as ButtonComponent, Drawer } from 'antd'; import styled from 'styled-components'; export const Container = styled.div` @@ -11,3 +11,10 @@ export const Button = styled(ButtonComponent)` align-items: center; } `; + +export const DrawerContainer = styled(Drawer)` + .ant-drawer-header { + padding: 0; + border: none; + } +`; diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 82627f8d22..eb38d54ae4 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -1,6 +1,7 @@ import { Button, Modal, Typography } from 'antd'; import ROUTES from 'constants/routes'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import history from 'lib/history'; import { DashboardWidgetPageParams } from 'pages/DashboardWidget'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; @@ -143,6 +144,7 @@ function NewWidget({ widgetId: selectedWidget?.id || '', graphType: selectedGraph, globalSelectedInterval, + variables: getDashboardVariables(), }); } }, [ diff --git a/frontend/src/lib/dashbaordVariables/customCommaValuesParser.ts b/frontend/src/lib/dashbaordVariables/customCommaValuesParser.ts new file mode 100644 index 0000000000..01b5ccc2eb --- /dev/null +++ b/frontend/src/lib/dashbaordVariables/customCommaValuesParser.ts @@ -0,0 +1,20 @@ +export const commaValuesParser = (query: string): (string | number)[] => { + if (!query) { + return []; + } + const match = query.match(/(?:\\,|[^,])+/g) ?? []; + + const options: string[] = match.map((text) => { + // eslint-disable-next-line no-param-reassign + text = text.replace(/\\,/g, ','); + const textMatch = /^(.+)\s:\s(.+)$/g.exec(text) ?? []; + if (textMatch.length === 3) { + const [, , value] = textMatch; + return value.trim(); + } + return text.trim(); + }); + return options.map((option): string | number => + Number.isNaN(Number(option)) ? option : Number(option), + ); +}; diff --git a/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts new file mode 100644 index 0000000000..1f5a66a2a2 --- /dev/null +++ b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts @@ -0,0 +1,38 @@ +import GetMinMax from 'lib/getMinMax'; +import GetStartAndEndTime from 'lib/getStartAndEndTime'; +import store from 'store'; + +export const getDashboardVariables = (): Record => { + try { + const { + globalTime, + dashboards: { dashboards }, + } = store.getState(); + const [selectedDashboard] = dashboards; + const { + data: { variables }, + } = selectedDashboard; + + const minMax = GetMinMax(globalTime.selectedTime, [ + globalTime.minTime / 1000000, + globalTime.maxTime / 1000000, + ]); + + const { start, end } = GetStartAndEndTime({ + type: 'GLOBAL_TIME', + minTime: minMax.minTime, + maxTime: minMax.maxTime, + }); + const variablesTuple: Record = { + SIGNOZ_START_TIME: parseInt(start, 10) * 1e3, + SIGNOZ_END_TIME: parseInt(end, 10) * 1e3, + }; + Object.keys(variables).forEach((key) => { + variablesTuple[key] = variables[key].selectedValue; + }); + return variablesTuple; + } catch (e) { + console.error(e); + } + return {}; +}; diff --git a/frontend/src/lib/dashbaordVariables/sortVariableValues.ts b/frontend/src/lib/dashbaordVariables/sortVariableValues.ts new file mode 100644 index 0000000000..0937f70412 --- /dev/null +++ b/frontend/src/lib/dashbaordVariables/sortVariableValues.ts @@ -0,0 +1,15 @@ +import { sortBy } from 'lodash-es'; +import { TSortVariableValuesType } from 'types/api/dashboard/getAll'; + +type TValuesDataType = (string | number | boolean)[]; +const sortValues = ( + values: TValuesDataType, + sortType: TSortVariableValuesType, +): TValuesDataType => { + if (sortType === 'ASC') return sortBy(values); + if (sortType === 'DESC') return sortBy(values).reverse(); + + return values; +}; + +export default sortValues; diff --git a/frontend/src/store/actions/dashboard/deleteWidget.ts b/frontend/src/store/actions/dashboard/deleteWidget.ts index 3aea4902b8..8509cdbc46 100644 --- a/frontend/src/store/actions/dashboard/deleteWidget.ts +++ b/frontend/src/store/actions/dashboard/deleteWidget.ts @@ -30,6 +30,7 @@ export const DeleteWidget = ({ tags: selectedDashboard.data.tags, widgets: updatedWidgets, layout: updatedLayout, + variables: selectedDashboard.data.variables, }, uuid: selectedDashboard.uuid, }; diff --git a/frontend/src/store/actions/dashboard/getQueryResults.ts b/frontend/src/store/actions/dashboard/getQueryResults.ts index 88cd3bbf07..1090c57bea 100644 --- a/frontend/src/store/actions/dashboard/getQueryResults.ts +++ b/frontend/src/store/actions/dashboard/getQueryResults.ts @@ -19,7 +19,7 @@ import { Dispatch } from 'redux'; import store from 'store'; import AppActions from 'types/actions'; import { ErrorResponse, SuccessResponse } from 'types/api'; -import { Query } from 'types/api/dashboard/getAll'; +import { IDashboardVariable, Query } from 'types/api/dashboard/getAll'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { EDataSource, EPanelType, EQueryType } from 'types/common/dashboard'; import { GlobalReducer } from 'types/reducer/globalTime'; @@ -29,11 +29,13 @@ export async function GetMetricQueryRange({ globalSelectedInterval, graphType, selectedTime, + variables = {}, }: { query: Query; graphType: GRAPH_TYPES; selectedTime: timePreferenceType; globalSelectedInterval: Time; + variables?: Record; }): Promise | ErrorResponse> { const { queryType } = query; const queryKey: Record = @@ -138,6 +140,7 @@ export async function GetMetricQueryRange({ start: parseInt(start, 10) * 1e3, end: parseInt(end, 10) * 1e3, step: getStep({ start, end, inputFormat: 'ms' }), + variables, ...QueryPayload, }); if (response.statusCode >= 400) { @@ -173,6 +176,14 @@ export const GetQueryResults = ( ): ((dispatch: Dispatch) => void) => { return async (dispatch: Dispatch): Promise => { try { + dispatch({ + type: 'QUERY_ERROR', + payload: { + errorMessage: '', + widgetId: props.widgetId, + errorBoolean: false, + }, + }); const response = await GetMetricQueryRange(props); const isError = response.error; @@ -199,14 +210,6 @@ export const GetQueryResults = ( }, }, }); - dispatch({ - type: 'QUERY_ERROR', - payload: { - errorMessage: '', - widgetId: props.widgetId, - errorBoolean: false, - }, - }); } catch (error) { dispatch({ type: 'QUERY_ERROR', @@ -226,4 +229,5 @@ export interface GetQueryResultsProps { query: Query; graphType: ITEMS; globalSelectedInterval: GlobalReducer['selectedTime']; + variables: Record; } diff --git a/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts b/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts new file mode 100644 index 0000000000..20bbbd0a06 --- /dev/null +++ b/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts @@ -0,0 +1,38 @@ +import { notification } from 'antd'; +import update from 'api/dashboard/update'; +import { Dispatch } from 'redux'; +import store from 'store/index'; +import AppActions from 'types/actions'; +import { UPDATE_DASHBOARD_VARIABLES } from 'types/actions/dashboard'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; + +export const UpdateDashboardVariables = ( + variables: Record, +): ((dispatch: Dispatch) => void) => { + return async (dispatch: Dispatch): Promise => { + try { + dispatch({ + type: UPDATE_DASHBOARD_VARIABLES, + payload: variables, + }); + + const reduxStoreState = store.getState(); + const [dashboard] = reduxStoreState.dashboards.dashboards; + + const response = await update({ + data: { + ...dashboard.data, + }, + uuid: dashboard.uuid, + }); + + if (response.statusCode !== 200) { + notification.error({ + message: response.error, + }); + } + } catch (error) { + console.error(error); + } + }; +}; diff --git a/frontend/src/store/reducers/dashboard.ts b/frontend/src/store/reducers/dashboard.ts index 61c5cc7351..0ef474ea32 100644 --- a/frontend/src/store/reducers/dashboard.ts +++ b/frontend/src/store/reducers/dashboard.ts @@ -18,6 +18,7 @@ import { SAVE_SETTING_TO_PANEL_SUCCESS, TOGGLE_EDIT_MODE, UPDATE_DASHBOARD, + UPDATE_DASHBOARD_VARIABLES, UPDATE_QUERY, UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS, } from 'types/actions/dashboard'; @@ -170,7 +171,6 @@ const dashboard = ( case QUERY_ERROR: { const { widgetId, errorMessage, errorBoolean = true } = action.payload; - const [selectedDashboard] = state.dashboards; const { data } = selectedDashboard; @@ -397,7 +397,25 @@ const dashboard = ( ], }; } + case UPDATE_DASHBOARD_VARIABLES: { + const variablesData = action.payload; + const { dashboards } = state; + const [selectedDashboard] = dashboards; + const { data } = selectedDashboard; + return { + ...state, + dashboards: [ + { + ...selectedDashboard, + data: { + ...data, + variables: variablesData, + }, + }, + ], + }; + } default: return state; } diff --git a/frontend/src/types/actions/dashboard.ts b/frontend/src/types/actions/dashboard.ts index 3330f028d4..4745771cc0 100644 --- a/frontend/src/types/actions/dashboard.ts +++ b/frontend/src/types/actions/dashboard.ts @@ -1,6 +1,11 @@ import { Layout } from 'react-grid-layout'; import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel'; -import { Dashboard, Query, Widgets } from 'types/api/dashboard/getAll'; +import { + Dashboard, + IDashboardVariable, + Query, + Widgets, +} from 'types/api/dashboard/getAll'; import { QueryData } from 'types/api/widgets/getQuery'; export const GET_DASHBOARD = 'GET_DASHBOARD'; @@ -42,6 +47,8 @@ export const IS_ADD_WIDGET = 'IS_ADD_WIDGET'; export const DELETE_QUERY = 'DELETE_QUERY'; export const FLUSH_DASHBOARD = 'FLUSH_DASHBOARD'; +export const UPDATE_DASHBOARD_VARIABLES = 'UPDATE_DASHBOARD_VARIABLES'; + interface GetDashboard { type: typeof GET_DASHBOARD; payload: Dashboard; @@ -174,6 +181,10 @@ interface DeleteQuery { interface FlushDashboard { type: typeof FLUSH_DASHBOARD; } +interface UpdateDashboardVariables { + type: typeof UPDATE_DASHBOARD_VARIABLES; + payload: Record; +} export type DashboardActions = | GetDashboard @@ -194,4 +205,5 @@ export type DashboardActions = | IsAddWidget | UpdateQuery | DeleteQuery - | FlushDashboard; + | FlushDashboard + | UpdateDashboardVariables; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index c158bff826..2a2617bb99 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -11,6 +11,31 @@ import { QueryData } from '../widgets/getQuery'; export type PayloadProps = Dashboard[]; +export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; +export type TVariableQueryType = typeof VariableQueryTypeArr[number]; + +export const VariableSortTypeArr = ['DISABLED', 'ASC', 'DESC'] as const; +export type TSortVariableValuesType = typeof VariableSortTypeArr[number]; + +export interface IDashboardVariable { + name?: string; // key will be the source of truth + description: string; + type: TVariableQueryType; + // Query + queryValue?: string; + // Custom + customValue?: string; + // Textbox + textboxValue?: string; + + sort: TSortVariableValuesType; + multiSelect: boolean; + showALLOption: boolean; + selectedValue?: null | string | string[]; + // Internal use + modificationUUID?: string; + allSelected?: boolean; +} export interface Dashboard { id: number; uuid: string; @@ -26,6 +51,7 @@ export interface DashboardData { widgets?: Widgets[]; title: string; layout?: Layout[]; + variables: Record; } export interface IBaseWidget { diff --git a/frontend/src/types/api/dashboard/variables/query.ts b/frontend/src/types/api/dashboard/variables/query.ts new file mode 100644 index 0000000000..e715378c66 --- /dev/null +++ b/frontend/src/types/api/dashboard/variables/query.ts @@ -0,0 +1,7 @@ +export type Props = { + query: string; +}; + +export type PayloadProps = { + variableValues: string[] | number[]; +}; diff --git a/frontend/src/types/api/metrics/getQueryRange.ts b/frontend/src/types/api/metrics/getQueryRange.ts index da28970542..bbe9c697a9 100644 --- a/frontend/src/types/api/metrics/getQueryRange.ts +++ b/frontend/src/types/api/metrics/getQueryRange.ts @@ -5,5 +5,6 @@ export interface MetricRangePayloadProps { data: { result: QueryData[]; resultType: string; + variables: Record; }; }