mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-30 23:32:01 +08:00
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:
parent
9e6d9019f7
commit
461a15d52d
26
frontend/src/api/dashboard/variables/query.ts
Normal file
26
frontend/src/api/dashboard/variables/query.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, 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;
|
@ -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(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -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,
|
||||
|
@ -27,6 +27,7 @@ export const UpdateDashboard = async ({
|
||||
description: data.description,
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
variables: data.variables,
|
||||
widgets: [
|
||||
...(data.widgets || []),
|
||||
{
|
||||
|
@ -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];
|
||||
|
@ -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);
|
@ -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;
|
||||
}
|
||||
`;
|
@ -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;
|
@ -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;
|
||||
`;
|
@ -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);
|
@ -0,0 +1 @@
|
||||
export type TVariableViewMode = 'EDIT' | 'ADD';
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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]};
|
||||
`;
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
@ -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(),
|
||||
});
|
||||
}
|
||||
}, [
|
||||
|
@ -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),
|
||||
);
|
||||
};
|
38
frontend/src/lib/dashbaordVariables/getDashboardVariables.ts
Normal file
38
frontend/src/lib/dashbaordVariables/getDashboardVariables.ts
Normal 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 {};
|
||||
};
|
15
frontend/src/lib/dashbaordVariables/sortVariableValues.ts
Normal file
15
frontend/src/lib/dashbaordVariables/sortVariableValues.ts
Normal 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;
|
@ -30,6 +30,7 @@ export const DeleteWidget = ({
|
||||
tags: selectedDashboard.data.tags,
|
||||
widgets: updatedWidgets,
|
||||
layout: updatedLayout,
|
||||
variables: selectedDashboard.data.variables,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
7
frontend/src/types/api/dashboard/variables/query.ts
Normal file
7
frontend/src/types/api/dashboard/variables/query.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export type Props = {
|
||||
query: string;
|
||||
};
|
||||
|
||||
export type PayloadProps = {
|
||||
variableValues: string[] | number[];
|
||||
};
|
@ -5,5 +5,6 @@ export interface MetricRangePayloadProps {
|
||||
data: {
|
||||
result: QueryData[];
|
||||
resultType: string;
|
||||
variables: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user