feat: dashboard variables (#1552)

* feat: dashboard variables
* fix: variable wipe on few instances
* feat: error handling states
* feat: eslint and tsc fixes
This commit is contained in:
Pranshu Chittora 2022-09-09 17:43:25 +05:30 committed by GitHub
parent 9e6d9019f7
commit 461a15d52d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1254 additions and 135 deletions

View File

@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/variables/query';
const query = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | 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;

View File

@ -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(),
}),
);

View File

@ -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<void> => {
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]);

View File

@ -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,

View File

@ -27,6 +27,7 @@ export const UpdateDashboard = async ({
description: data.description,
name: data.name,
tags: data.tags,
variables: data.variables,
widgets: [
...(data.widgets || []),
{

View File

@ -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];

View File

@ -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<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const selectedData = selectedDashboard.data;
const { title } = selectedData;
const { tags } = selectedData;
const { description } = selectedData;
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
const [updatedTags, setUpdatedTags] = useState<string[]>(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 (
<Col>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Typography style={{ marginBottom: '0.5rem' }}>Name</Typography>
<Input
value={updatedTitle}
onChange={(e): void => setUpdatedTitle(e.target.value)}
/>
</div>
<div>
<Typography style={{ marginBottom: '0.5rem' }}>Description</Typography>
<Input.TextArea
value={updatedDescription}
onChange={(e): void => setUpdatedDescription(e.target.value)}
/>
</div>
<div>
<Typography style={{ marginBottom: '0.5rem' }}>Tags</Typography>
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
</div>
<div>
<Divider />
<Button icon={<SaveOutlined />} onClick={onSaveHandler} type="primary">
{t('save')}
</Button>
</div>
</Space>
</Col>
);
}
interface DispatchProps {
updateDashboardTitleDescriptionTags: (
props: UpdateDashboardTitleDescriptionTagsProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardTitleDescriptionTags: bindActionCreators(
UpdateDashboardTitleDescriptionTags,
dispatch,
),
});
type DescriptionOfDashboardProps = DispatchProps;
export default connect(null, mapDispatchToProps)(GeneralDashboardSettings);

View File

@ -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;
}
`;

View File

@ -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<string>(
variableData.name || '',
);
const [variableDescription, setVariableDescription] = useState<string>(
variableData.description || '',
);
const [queryType, setQueryType] = useState<TVariableQueryType>(
variableData.type || 'QUERY',
);
const [variableQueryValue, setVariableQueryValue] = useState<string>(
variableData.queryValue || '',
);
const [variableCustomValue, setVariableCustomValue] = useState<string>(
variableData.customValue || '',
);
const [variableTextboxValue, setVariableTextboxValue] = useState<string>(
variableData.textboxValue || '',
);
const [
variableSortType,
setVariableSortType,
] = useState<TSortVariableValuesType>(
variableData.sort || VariableSortTypeArr[0],
);
const [variableMultiSelect, setVariableMultiSelect] = useState<boolean>(
variableData.multiSelect || false,
);
const [variableShowALLOption, setVariableShowALLOption] = useState<boolean>(
variableData.showALLOption || false,
);
const [previewValues, setPreviewValues] = useState<string[]>([]);
// Internal states
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
// Error messages
const [errorName, setErrorName] = useState<boolean>(false);
const [errorPreview, setErrorPreview] = useState<string | null>(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<void> => {
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 (
<Col>
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
<VariableItemRow>
<LabelContainer>
<Typography>Name</Typography>
</LabelContainer>
<div>
<Input
placeholder="Unique name of the variable"
style={{ width: 400 }}
value={variableName}
onChange={(e): void => {
setVariableName(e.target.value);
setErrorName(!validateName(e.target.value));
}}
/>
<div>
<Typography.Text type="warning">
{errorName ? 'Variable name already exists' : ''}
</Typography.Text>
</div>
</div>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Description</Typography>
</LabelContainer>
<Input.TextArea
value={variableDescription}
placeholder="Write description of the variable"
style={{ width: 400 }}
onChange={(e): void => setVariableDescription(e.target.value)}
/>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Type</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
style={{ width: 400 }}
onChange={(e: TVariableQueryType): void => {
setQueryType(e);
}}
value={queryType}
>
<Option value={VariableQueryTypeArr[0]}>Query</Option>
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
</Select>
</VariableItemRow>
<Typography.Title
level={5}
style={{ marginTop: '1rem', marginBottom: '1rem' }}
>
Options
</Typography.Title>
{queryType === 'QUERY' && (
<VariableItemRow>
<LabelContainer>
<Typography>Query</Typography>
</LabelContainer>
<div style={{ flex: 1, position: 'relative' }}>
<Editor
language="sql"
value={variableQueryValue}
onChange={(e): void => setVariableQueryValue(e)}
height="300px"
/>
<Button
type="primary"
onClick={handleQueryResult}
style={{
position: 'absolute',
bottom: 0,
}}
loading={previewLoading}
>
Test Run Query
</Button>
</div>
</VariableItemRow>
)}
{queryType === 'CUSTOM' && (
<VariableItemRow>
<LabelContainer>
<Typography>Values separated by comma</Typography>
</LabelContainer>
<Input.TextArea
value={variableCustomValue}
placeholder="1, 10, mykey, mykey:myvalue"
style={{ width: 400 }}
onChange={(e): void => {
setVariableCustomValue(e.target.value);
setPreviewValues(
sortValues(
commaValuesParser(e.target.value),
variableSortType,
) as never,
);
}}
/>
</VariableItemRow>
)}
{queryType === 'TEXTBOX' && (
<VariableItemRow>
<LabelContainer>
<Typography>Default Value</Typography>
</LabelContainer>
<Input
value={variableTextboxValue}
onChange={(e): void => {
setVariableTextboxValue(e.target.value);
}}
placeholder="Default value if any"
style={{ width: 400 }}
/>
</VariableItemRow>
)}
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
<>
<VariableItemRow>
<LabelContainer>
<Typography>Preview of Values</Typography>
</LabelContainer>
<div style={{ flex: 1 }}>
{errorPreview ? (
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
) : (
map(previewValues, (value, idx) => (
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
))
)}
</div>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Sort</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
style={{ width: 400 }}
defaultValue={VariableSortTypeArr[0]}
value={variableSortType}
onChange={(value: TSortVariableValuesType): void =>
setVariableSortType(value)
}
>
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
<Option value={VariableSortTypeArr[2]}>Descending</Option>
</Select>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Enable multiple values to be checked</Typography>
</LabelContainer>
<Switch
checked={variableMultiSelect}
onChange={(e): void => {
setVariableMultiSelect(e);
if (!e) {
setVariableShowALLOption(false);
}
}}
/>
</VariableItemRow>
{variableMultiSelect && (
<VariableItemRow>
<LabelContainer>
<Typography>Include an option for ALL values</Typography>
</LabelContainer>
<Switch
checked={variableShowALLOption}
onChange={(e): void => setVariableShowALLOption(e)}
/>
</VariableItemRow>
)}
</>
)}
<Divider />
<VariableItemRow>
<Button type="primary" onClick={handleSave} disabled={errorName}>
Save
</Button>
<Button type="dashed" onClick={onCancel}>
Cancel
</Button>
</VariableItemRow>
</Col>
);
}
export default VariableItem;

View File

@ -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;
`;

View File

@ -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<string | null>(null);
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
const { dashboards } = useSelector<AppState, DashboardReducer>(
(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 | TVariableViewMode>(null);
const [
variableEditData,
setVariableEditData,
] = useState<null | IDashboardVariable>(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 => (
<Space>
<Button
type="text"
style={{ padding: 0, cursor: 'pointer', color: blue[5] }}
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
>
Edit
</Button>
<Button
type="text"
style={{ padding: 0, color: red[6], cursor: 'pointer' }}
onClick={(): void => {
if (_.name) onVariableDeleteHandler(_.name);
}}
>
Delete
</Button>
</Space>
),
},
];
return (
<>
{variableViewMode ? (
<VariableItem
variableData={{ ...variableEditData } as IDashboardVariable}
onSave={onVariableSaveHandler}
onCancel={onDoneVariableViewMode}
validateName={validateVariableName}
variableViewMode={variableViewMode}
/>
) : (
<>
<Row style={{ flexDirection: 'row-reverse', padding: '0.5rem 0' }}>
<Button
type="primary"
onClick={(): void =>
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
}
>
<PlusOutlined /> New Variables
</Button>
</Row>
<Table columns={columns} dataSource={variablesTableData} />
</>
)}
<Modal
title="Delete variable"
centered
visible={deleteVariableModal}
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
>
Are you sure you want to delete variable{' '}
<Tag>{variableToDelete.current}</Tag>?
</Modal>
</>
);
}
interface DispatchProps {
updateDashboardVariables: (
props: Record<string, IDashboardVariable>,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardVariables: bindActionCreators(
UpdateDashboardVariables,
dispatch,
),
});
export default connect(null, mapDispatchToProps)(VariablesSetting);

View File

@ -0,0 +1 @@
export type TVariableViewMode = 'EDIT' | 'ADD';

View File

@ -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 (
<Tabs>
<TabPane tab="General" key="general">
<GeneralDashboardSettings />
</TabPane>
<TabPane tab="Variables" key="variables">
<VariablesSetting />
</TabPane>
</Tabs>
);
}
export default DashboardSettingsContent;

View File

@ -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<boolean>(false);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
const getOptions = useCallback(async (): Promise<void> => {
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 (
<VariableContainer>
<VariableName>${variableData.name}</VariableName>
{variableData.type === 'TEXTBOX' ? (
<Input
placeholder="Enter value"
bordered={false}
value={variableData.selectedValue?.toString()}
onChange={(e): void => {
handleChange(e.target.value || '');
}}
style={{
width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50),
}}
/>
) : (
<Select
value={variableData.allSelected ? 'ALL' : variableData.selectedValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
mode={
(variableData.multiSelect && !variableData.allSelected
? 'multiple'
: null) as never
}
dropdownMatchSelectWidth={false}
style={{
minWidth: 120,
fontSize: '0.8rem',
}}
loading={isLoading}
showArrow
>
{variableData.multiSelect && variableData.showALLOption && (
<Option value={ALL_SELECT_VALUE}>ALL</Option>
)}
{map(optionsData, (option) => {
return <Option value={option}>{(option as string).toString()}</Option>;
})}
</Select>
)}
{errorMessage && (
<span style={{ margin: '0 0.5rem' }}>
<Popover placement="top" content={<Typography>{errorMessage}</Typography>}>
<WarningOutlined style={{ color: orange[5] }} />
</Popover>
</span>
)}
</VariableContainer>
);
}
export default VariableItem;

View File

@ -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<AppState, DashboardReducer>(
(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 (
<Row style={{ gap: '1rem' }}>
{map(sortBy(Object.keys(variables)), (variableName) => (
<VariableItem
key={`${variableName}${variables[variableName].modificationUUID}`}
variableData={{ name: variableName, ...variables[variableName] }}
onValueUpdate={onValueUpdate as never}
onAllSelectedUpdate={onAllSelectedUpdate as never}
/>
))}
</Row>
);
}
interface DispatchProps {
updateDashboardVariables: (
props: Parameters<typeof UpdateDashboardVariables>[0],
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardVariables: bindActionCreators(
UpdateDashboardVariables,
dispatch,
),
});
export default connect(null, mapDispatchToProps)(DashboardVariableSelection);

View File

@ -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]};
`;

View File

@ -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 (
<>
<Button type="dashed" onClick={showDrawer}>
<SettingOutlined /> Configure
</Button>
<DrawerContainer
placement="right"
width="70%"
onClose={onClose}
visible={visible}
maskClosable={false}
>
<DashboardSettingsContent />
</DrawerContainer>
</>
);
}
export default SettingsDrawer;

View File

@ -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<AppState, DashboardReducer>(
function DescriptionOfDashboard(): JSX.Element {
const { dashboards } = useSelector<AppState, DashboardReducer>(
(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<string>(title);
const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []);
const [updatedDescription, setUpdatedDescription] = useState(
description || '',
);
const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false);
const { t } = useTranslation('common');
const { role } = useSelector<AppState, AppReducer>((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 (
<Card>
<Row align="top" justify="space-between">
{!isEditMode ? (
<Col>
<Typography>{title}</Typography>
<Container>
{tags?.map((e) => (
<Tag key={e}>{e}</Tag>
))}
</Container>
<Container>
<Typography>{description}</Typography>
</Container>
</Col>
) : (
<Col lg={8}>
<NameOfTheDashboard name={updatedTitle} setName={setUpdatedTitle} />
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
<Description
description={updatedDescription}
setDescription={setUpdatedDescription}
/>
</Col>
)}
<ShareModal
{...{
isJSONModalVisible,
onToggleHandler,
selectedData,
}}
/>
<Row>
<Col style={{ flex: 1 }}>
<Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
{title}
</Typography.Title>
<Typography>{description}</Typography>
<div style={{ margin: '0.5rem 0' }}>
{tags?.map((e) => (
<Tag key={e}>{e}</Tag>
))}
</div>
<DashboardVariableSelection />
</Col>
<Col>
<ShareModal
{...{
isJSONModalVisible,
onToggleHandler,
selectedData,
}}
/>
<Space direction="vertical">
<Button onClick={onToggleHandler} icon={<ShareAltOutlined />}>
{editDashboard && <SettingsDrawer />}
<Button
style={{ width: '100%' }}
type="dashed"
onClick={onToggleHandler}
icon={<ShareAltOutlined />}
>
{t('share')}
</Button>
{editDashboard && (
<Button
icon={!isEditMode ? <EditOutlined /> : <SaveOutlined />}
onClick={onClickEditHandler}
>
{isEditMode ? t('save') : t('edit')}
</Button>
)}
</Space>
</Col>
</Row>
@ -137,23 +71,4 @@ function DescriptionOfDashboard({
);
}
interface DispatchProps {
updateDashboardTitleDescriptionTags: (
props: UpdateDashboardTitleDescriptionTagsProps,
) => (dispatch: Dispatch<AppActions>) => void;
toggleEditMode: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardTitleDescriptionTags: bindActionCreators(
UpdateDashboardTitleDescriptionTags,
dispatch,
),
toggleEditMode: bindActionCreators(ToggleEditMode, dispatch),
});
type DescriptionOfDashboardProps = DispatchProps;
export default connect(null, mapDispatchToProps)(DescriptionOfDashboard);
export default DescriptionOfDashboard;

View File

@ -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;
}
`;

View File

@ -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(),
});
}
}, [

View File

@ -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),
);
};

View File

@ -0,0 +1,38 @@
import GetMinMax from 'lib/getMinMax';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import store from 'store';
export const getDashboardVariables = (): Record<string, unknown> => {
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<string, unknown> = {
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 {};
};

View File

@ -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;

View File

@ -30,6 +30,7 @@ export const DeleteWidget = ({
tags: selectedDashboard.data.tags,
widgets: updatedWidgets,
layout: updatedLayout,
variables: selectedDashboard.data.variables,
},
uuid: selectedDashboard.uuid,
};

View File

@ -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<string, unknown>;
}): Promise<SuccessResponse<MetricRangePayloadProps> | ErrorResponse> {
const { queryType } = query;
const queryKey: Record<EQueryTypeToQueryKeyMapping, string> =
@ -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<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
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<string, unknown>;
}

View File

@ -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<string, IDashboardVariable>,
): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
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);
}
};
};

View File

@ -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;
}

View File

@ -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<string, IDashboardVariable>;
}
export type DashboardActions =
| GetDashboard
@ -194,4 +205,5 @@ export type DashboardActions =
| IsAddWidget
| UpdateQuery
| DeleteQuery
| FlushDashboard;
| FlushDashboard
| UpdateDashboardVariables;

View File

@ -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<string, IDashboardVariable>;
}
export interface IBaseWidget {

View File

@ -0,0 +1,7 @@
export type Props = {
query: string;
};
export type PayloadProps = {
variableValues: string[] | number[];
};

View File

@ -5,5 +5,6 @@ export interface MetricRangePayloadProps {
data: {
result: QueryData[];
resultType: string;
variables: Record<string, unknown>;
};
}