mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 06:19:03 +08:00
feat: dashboard search and filter (#1005)
* feat: enable search and filter in dashboards
This commit is contained in:
parent
2b5b79e34a
commit
182adc551c
@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
|
*.typegen.ts
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
"@welldone-software/why-did-you-render": "^6.2.1",
|
"@welldone-software/why-did-you-render": "^6.2.1",
|
||||||
|
"@xstate/react": "^3.0.0",
|
||||||
"antd": "4.19.2",
|
"antd": "4.19.2",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
@ -58,6 +59,7 @@
|
|||||||
"i18next-browser-languagedetector": "^6.1.3",
|
"i18next-browser-languagedetector": "^6.1.3",
|
||||||
"i18next-http-backend": "^1.3.2",
|
"i18next-http-backend": "^1.3.2",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
|
"js-base64": "^3.7.2",
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
@ -85,7 +87,8 @@
|
|||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web-vitals": "^0.2.4",
|
"web-vitals": "^0.2.4",
|
||||||
"webpack": "^5.23.0",
|
"webpack": "^5.23.0",
|
||||||
"webpack-dev-server": "^4.3.1"
|
"webpack-dev-server": "^4.3.1",
|
||||||
|
"xstate": "^4.31.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import { createMachine } from 'xstate';
|
||||||
|
|
||||||
|
export const DashboardSearchAndFilter = createMachine({
|
||||||
|
tsTypes: {} as import('./Dashboard.machine.typegen').Typegen0,
|
||||||
|
initial: 'Idle',
|
||||||
|
states: {
|
||||||
|
Category: {
|
||||||
|
on: {
|
||||||
|
NEXT: {
|
||||||
|
actions: 'onSelectOperator',
|
||||||
|
target: 'Operator',
|
||||||
|
},
|
||||||
|
onBlur: {
|
||||||
|
actions: 'onBlurPurge',
|
||||||
|
target: 'Idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Operator: {
|
||||||
|
on: {
|
||||||
|
NEXT: {
|
||||||
|
actions: 'onSelectValue',
|
||||||
|
target: 'Value',
|
||||||
|
},
|
||||||
|
onBlur: {
|
||||||
|
actions: 'onBlurPurge',
|
||||||
|
target: 'Idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: {
|
||||||
|
on: {
|
||||||
|
onBlur: {
|
||||||
|
actions: ['onValidateQuery', 'onBlurPurge'],
|
||||||
|
target: 'Idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Idle: {
|
||||||
|
on: {
|
||||||
|
NEXT: {
|
||||||
|
actions: 'onSelectCategory',
|
||||||
|
description: 'Select Category',
|
||||||
|
target: 'Category',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: 'Dashboard Search And Filter',
|
||||||
|
});
|
@ -0,0 +1,32 @@
|
|||||||
|
// This file was automatically generated. Edits will be overwritten
|
||||||
|
|
||||||
|
export interface Typegen0 {
|
||||||
|
'@@xstate/typegen': true;
|
||||||
|
eventsCausingActions: {
|
||||||
|
onSelectOperator: 'NEXT';
|
||||||
|
onBlurPurge: 'onBlur';
|
||||||
|
onSelectValue: 'NEXT';
|
||||||
|
onValidateQuery: 'onBlur';
|
||||||
|
onSelectCategory: 'NEXT';
|
||||||
|
};
|
||||||
|
internalEvents: {
|
||||||
|
'xstate.init': { type: 'xstate.init' };
|
||||||
|
};
|
||||||
|
invokeSrcNameMap: {};
|
||||||
|
missingImplementations: {
|
||||||
|
actions:
|
||||||
|
| 'onSelectOperator'
|
||||||
|
| 'onBlurPurge'
|
||||||
|
| 'onSelectValue'
|
||||||
|
| 'onValidateQuery'
|
||||||
|
| 'onSelectCategory';
|
||||||
|
services: never;
|
||||||
|
guards: never;
|
||||||
|
delays: never;
|
||||||
|
};
|
||||||
|
eventsCausingServices: {};
|
||||||
|
eventsCausingGuards: {};
|
||||||
|
eventsCausingDelays: {};
|
||||||
|
matchesStates: 'Category' | 'Operator' | 'Value' | 'Idle';
|
||||||
|
tags: never;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { QueryChipContainer, QueryChipItem } from './styles';
|
||||||
|
import { IQueryStructure } from './types';
|
||||||
|
|
||||||
|
export default function QueryChip({
|
||||||
|
queryData,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
queryData: IQueryStructure;
|
||||||
|
onRemove: (id: string) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { category, operator, value, id } = queryData;
|
||||||
|
return (
|
||||||
|
<QueryChipContainer>
|
||||||
|
<QueryChipItem>{category}</QueryChipItem>
|
||||||
|
<QueryChipItem>{operator}</QueryChipItem>
|
||||||
|
<QueryChipItem closable onClose={(): void => onRemove(id)}>
|
||||||
|
{Array.isArray(value) ? value.join(', ') : null}
|
||||||
|
</QueryChipItem>
|
||||||
|
</QueryChipContainer>
|
||||||
|
);
|
||||||
|
}
|
216
frontend/src/container/ListOfDashboard/SearchFilter/index.tsx
Normal file
216
frontend/src/container/ListOfDashboard/SearchFilter/index.tsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import { CloseCircleFilled } from '@ant-design/icons';
|
||||||
|
import { useMachine } from '@xstate/react';
|
||||||
|
import { Button, Select } from 'antd';
|
||||||
|
import { RefSelectProps } from 'antd/lib/select';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { filter, map } from 'lodash-es';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { DashboardSearchAndFilter } from './Dashboard.machine';
|
||||||
|
import QueryChip from './QueryChip';
|
||||||
|
import { QueryChipItem, SearchContainer } from './styles';
|
||||||
|
import { IOptionsData, IQueryStructure, TCategory, TOperator } from './types';
|
||||||
|
import {
|
||||||
|
convertQueriesToURLQuery,
|
||||||
|
convertURLQueryStringToQuery,
|
||||||
|
executeSearchQueries,
|
||||||
|
OptionsSchemas,
|
||||||
|
OptionsValueResolution,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function SearchFilter({
|
||||||
|
searchData,
|
||||||
|
filterDashboards,
|
||||||
|
}: {
|
||||||
|
searchData: Dashboard[];
|
||||||
|
filterDashboards: (filteredDashboards: Dashboard[]) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
const [category, setCategory] = useState<TCategory>();
|
||||||
|
const [optionsData, setOptionsData] = useState<IOptionsData>(
|
||||||
|
OptionsSchemas.attribute,
|
||||||
|
);
|
||||||
|
const selectRef = useRef() as React.MutableRefObject<RefSelectProps>;
|
||||||
|
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||||
|
const [staging, setStaging] = useState<string[] | string[][] | unknown[]>([]);
|
||||||
|
const [queries, setQueries] = useState<IQueryStructure[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const searchQueryString = new URLSearchParams(history.location.search).get(
|
||||||
|
'search',
|
||||||
|
);
|
||||||
|
if (searchQueryString)
|
||||||
|
setQueries(convertURLQueryStringToQuery(searchQueryString) || []);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
filterDashboards(executeSearchQueries(queries, searchData));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [queries, searchData]);
|
||||||
|
|
||||||
|
const updateURLWithQuery = useCallback(
|
||||||
|
(inputQueries?: IQueryStructure[]): void => {
|
||||||
|
history.push({
|
||||||
|
pathname: history.location.pathname,
|
||||||
|
search:
|
||||||
|
inputQueries || queries
|
||||||
|
? `?search=${convertQueriesToURLQuery(inputQueries || queries)}`
|
||||||
|
: '',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[queries],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Array.isArray(queries) && queries.length > 0) {
|
||||||
|
updateURLWithQuery();
|
||||||
|
}
|
||||||
|
}, [queries, updateURLWithQuery]);
|
||||||
|
|
||||||
|
const [state, send] = useMachine(DashboardSearchAndFilter, {
|
||||||
|
actions: {
|
||||||
|
onSelectCategory: () => {
|
||||||
|
setOptionsData(OptionsSchemas.attribute);
|
||||||
|
},
|
||||||
|
onSelectOperator: () => {
|
||||||
|
setOptionsData(OptionsSchemas.operator);
|
||||||
|
},
|
||||||
|
onSelectValue: () => {
|
||||||
|
setOptionsData(
|
||||||
|
OptionsValueResolution(category as TCategory, searchData) as IOptionsData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onBlurPurge: () => {
|
||||||
|
setSelectedValues([]);
|
||||||
|
setStaging([]);
|
||||||
|
},
|
||||||
|
onValidateQuery: () => {
|
||||||
|
if (staging.length <= 2 && selectedValues.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setQueries([
|
||||||
|
...queries,
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
category: staging[0] as string,
|
||||||
|
operator: staging[1] as TOperator,
|
||||||
|
value: selectedValues,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = (): void => {
|
||||||
|
send('NEXT');
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeQueryById = (queryId: string): void => {
|
||||||
|
setQueries((queries) => {
|
||||||
|
const updatedQueries = filter(queries, ({ id }) => id !== queryId);
|
||||||
|
updateURLWithQuery(updatedQueries);
|
||||||
|
return updatedQueries;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (value: never | string[]): void => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (optionsData.mode) {
|
||||||
|
setSelectedValues(value.filter(Boolean));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStaging([...staging, value]);
|
||||||
|
|
||||||
|
if (state.value === 'Category') {
|
||||||
|
setCategory(`${value}`.toLowerCase() as TCategory);
|
||||||
|
}
|
||||||
|
nextState();
|
||||||
|
setSelectedValues([]);
|
||||||
|
};
|
||||||
|
const handleFocus = (): void => {
|
||||||
|
if (state.value === 'Idle') {
|
||||||
|
send('NEXT');
|
||||||
|
selectRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (): void => {
|
||||||
|
send('onBlur');
|
||||||
|
selectRef?.current?.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearQueries = (): void => {
|
||||||
|
setQueries([]);
|
||||||
|
history.push({
|
||||||
|
pathname: history.location.pathname,
|
||||||
|
search: ``,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchContainer isDarkMode={isDarkMode}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: '70%',
|
||||||
|
display: 'flex',
|
||||||
|
overflowX: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{map(queries, (query) => (
|
||||||
|
<QueryChip key={query.id} queryData={query} onRemove={removeQueryById} />
|
||||||
|
))}
|
||||||
|
{map(staging, (value) => (
|
||||||
|
<QueryChipItem key={JSON.stringify(value)}>
|
||||||
|
{value as string}
|
||||||
|
</QueryChipItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{optionsData && (
|
||||||
|
<Select
|
||||||
|
placeholder={
|
||||||
|
!queries.length &&
|
||||||
|
!staging.length &&
|
||||||
|
!selectedValues.length &&
|
||||||
|
'Search or Filter results'
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
ref={selectRef}
|
||||||
|
mode={optionsData.mode as 'tags' | 'multiple'}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onChange={handleChange}
|
||||||
|
bordered={false}
|
||||||
|
suffixIcon={null}
|
||||||
|
value={selectedValues}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
>
|
||||||
|
{optionsData.options &&
|
||||||
|
Array.isArray(optionsData.options) &&
|
||||||
|
optionsData.options.map(
|
||||||
|
(optionItem): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Select.Option
|
||||||
|
key={(optionItem.value as string) || (optionItem.name as string)}
|
||||||
|
value={optionItem.value || optionItem.name}
|
||||||
|
>
|
||||||
|
{optionItem.name}
|
||||||
|
</Select.Option>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{queries && queries.length > 0 && (
|
||||||
|
<Button icon={<CloseCircleFilled />} type="text" onClick={clearQueries} />
|
||||||
|
)}
|
||||||
|
</SearchContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchFilter;
|
@ -0,0 +1,30 @@
|
|||||||
|
import { grey } from '@ant-design/colors';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const SearchContainer = styled.div<{
|
||||||
|
isDarkMode: boolean;
|
||||||
|
}>`
|
||||||
|
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
border: 1px solid #ccc5;
|
||||||
|
`;
|
||||||
|
export const QueryChipContainer = styled.span`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
&:hover {
|
||||||
|
& > * {
|
||||||
|
background: ${grey.primary}44;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const QueryChipItem = styled(Tag)`
|
||||||
|
margin-right: 0.1rem;
|
||||||
|
`;
|
18
frontend/src/container/ListOfDashboard/SearchFilter/types.ts
Normal file
18
frontend/src/container/ListOfDashboard/SearchFilter/types.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export type TOperator = '=' | '!=';
|
||||||
|
|
||||||
|
export type TCategory = 'title' | 'description' | 'tags';
|
||||||
|
export interface IQueryStructure {
|
||||||
|
category: string;
|
||||||
|
id: string;
|
||||||
|
operator: TOperator;
|
||||||
|
value: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IOptions {
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
export interface IOptionsData {
|
||||||
|
mode: undefined | 'tags' | 'multiple';
|
||||||
|
options: IOptions[] | [];
|
||||||
|
}
|
152
frontend/src/container/ListOfDashboard/SearchFilter/utils.ts
Normal file
152
frontend/src/container/ListOfDashboard/SearchFilter/utils.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import { decode, encode } from 'js-base64';
|
||||||
|
import { flattenDeep, map, uniqWith } from 'lodash-es';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import { IOptionsData, IQueryStructure, TCategory, TOperator } from './types';
|
||||||
|
|
||||||
|
export const convertQueriesToURLQuery = (
|
||||||
|
queries: IQueryStructure[],
|
||||||
|
): string => {
|
||||||
|
if (!queries || !queries.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return encode(JSON.stringify(queries));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertURLQueryStringToQuery = (
|
||||||
|
queryString: string,
|
||||||
|
): IQueryStructure[] => {
|
||||||
|
return JSON.parse(decode(queryString));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveOperator = (
|
||||||
|
result: unknown,
|
||||||
|
operator: TOperator,
|
||||||
|
): boolean => {
|
||||||
|
if (operator === '!=') {
|
||||||
|
return !result;
|
||||||
|
}
|
||||||
|
if (operator === '=') {
|
||||||
|
return !!result;
|
||||||
|
}
|
||||||
|
return !!result;
|
||||||
|
};
|
||||||
|
export const executeSearchQueries = (
|
||||||
|
queries: IQueryStructure[] = [],
|
||||||
|
searchData: Dashboard[] = [],
|
||||||
|
): Dashboard[] => {
|
||||||
|
if (!searchData.length || !queries.length) {
|
||||||
|
return searchData;
|
||||||
|
}
|
||||||
|
|
||||||
|
queries.forEach((query: IQueryStructure) => {
|
||||||
|
const { operator } = query;
|
||||||
|
let { value } = query;
|
||||||
|
const categoryLowercase: TCategory = `${query.category}`.toLowerCase() as
|
||||||
|
| 'title'
|
||||||
|
| 'description';
|
||||||
|
value = flattenDeep([value]);
|
||||||
|
|
||||||
|
searchData = searchData.filter(({ data: searchPayload }: Dashboard) => {
|
||||||
|
try {
|
||||||
|
const searchSpace =
|
||||||
|
flattenDeep([searchPayload[categoryLowercase]]).filter(Boolean) || null;
|
||||||
|
if (!searchSpace || !searchSpace.length)
|
||||||
|
return resolveOperator(false, operator);
|
||||||
|
|
||||||
|
for (const searchSpaceItem of searchSpace) {
|
||||||
|
if (searchSpaceItem)
|
||||||
|
for (const queryValue of value) {
|
||||||
|
if (searchSpaceItem.match(queryValue)) {
|
||||||
|
return resolveOperator(true, operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
return resolveOperator(false, operator);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return searchData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OptionsSchemas = {
|
||||||
|
attribute: {
|
||||||
|
mode: undefined,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tags',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
operator: {
|
||||||
|
mode: undefined,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: '=',
|
||||||
|
name: 'Equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Equal',
|
||||||
|
value: '!=',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OptionsValueResolution(
|
||||||
|
category: TCategory,
|
||||||
|
searchData: Dashboard[],
|
||||||
|
): Record<string, unknown> | IOptionsData {
|
||||||
|
const OptionsValueSchema = {
|
||||||
|
title: {
|
||||||
|
mode: 'tags',
|
||||||
|
options: uniqWith(
|
||||||
|
map(searchData, (searchItem) => ({ name: searchItem.data.title })),
|
||||||
|
(prev, next) => prev.name === next.name,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
mode: 'tags',
|
||||||
|
options: uniqWith(
|
||||||
|
map(searchData, (searchItem) =>
|
||||||
|
searchItem.data.description
|
||||||
|
? {
|
||||||
|
name: searchItem.data.description,
|
||||||
|
value: searchItem.data.description,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
).filter(Boolean),
|
||||||
|
(prev, next) => prev?.name === next?.name,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
mode: 'tags',
|
||||||
|
options: uniqWith(
|
||||||
|
map(
|
||||||
|
flattenDeep(
|
||||||
|
map(searchData, (searchItem) => searchItem.data.tags).filter(Boolean),
|
||||||
|
),
|
||||||
|
(tag) => ({ name: tag }),
|
||||||
|
),
|
||||||
|
(prev, next) => prev.name === next.name,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
OptionsValueSchema[category] ||
|
||||||
|
({ mode: undefined, options: [] } as IOptionsData)
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
interface IDashboardSearchData {
|
||||||
|
title: string;
|
||||||
|
description: string | undefined;
|
||||||
|
tags: string[];
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateSearchData = (
|
||||||
|
dashboards: Dashboard[],
|
||||||
|
): IDashboardSearchData[] => {
|
||||||
|
const dashboardSearchData: IDashboardSearchData[] = [];
|
||||||
|
|
||||||
|
dashboards.forEach((dashboard) => {
|
||||||
|
dashboardSearchData.push({
|
||||||
|
id: dashboard.uuid,
|
||||||
|
title: dashboard.data.title,
|
||||||
|
description: dashboard.data.description,
|
||||||
|
tags: dashboard.data.tags || [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return dashboardSearchData;
|
||||||
|
};
|
@ -1,15 +1,17 @@
|
|||||||
/* eslint-disable react/no-unstable-nested-components */
|
/* eslint-disable react/no-unstable-nested-components */
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Row, Table, TableColumnProps, Typography } from 'antd';
|
import { Card, Row, Table, TableColumnProps, Typography } from 'antd';
|
||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
@ -25,6 +27,11 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [filteredDashboards, setFilteredDashboards] = useState<Dashboard[]>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilteredDashboards(dashboards);
|
||||||
|
}, [dashboards]);
|
||||||
const [newDashboardState, setNewDashboardState] = useState({
|
const [newDashboardState, setNewDashboardState] = useState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
@ -76,7 +83,7 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const data: Data[] = dashboards.map((e) => ({
|
const data: Data[] = (filteredDashboards || dashboards).map((e) => ({
|
||||||
createdBy: e.created_at,
|
createdBy: e.created_at,
|
||||||
description: e.data.description || '',
|
description: e.data.description || '',
|
||||||
id: e.uuid,
|
id: e.uuid,
|
||||||
@ -138,47 +145,52 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<Card>
|
||||||
<Table
|
<Row justify="space-between">
|
||||||
pagination={{
|
<Typography>Dashboard List</Typography>
|
||||||
pageSize: 9,
|
|
||||||
defaultPageSize: 9,
|
|
||||||
}}
|
|
||||||
showHeader
|
|
||||||
bordered
|
|
||||||
sticky
|
|
||||||
loading={loading}
|
|
||||||
title={(): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Row justify="space-between">
|
|
||||||
<Typography>Dashboard List</Typography>
|
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<TextToolTip
|
<TextToolTip
|
||||||
{...{
|
{...{
|
||||||
text: `More details on how to create dashboards`,
|
text: `More details on how to create dashboards`,
|
||||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NewDashboardButton
|
<NewDashboardButton
|
||||||
onClick={onNewDashboardHandler}
|
onClick={onNewDashboardHandler}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
type="primary"
|
type="primary"
|
||||||
loading={newDashboardState.loading}
|
loading={newDashboardState.loading}
|
||||||
danger={newDashboardState.error}
|
danger={newDashboardState.error}
|
||||||
>
|
>
|
||||||
{getText()}
|
{getText()}
|
||||||
</NewDashboardButton>
|
</NewDashboardButton>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
{!loading && (
|
||||||
}}
|
<SearchFilter
|
||||||
columns={columns}
|
searchData={dashboards}
|
||||||
dataSource={data}
|
filterDashboards={setFilteredDashboards}
|
||||||
showSorterTooltip
|
/>
|
||||||
/>
|
)}
|
||||||
</TableContainer>
|
|
||||||
|
<TableContainer>
|
||||||
|
<Table
|
||||||
|
pagination={{
|
||||||
|
pageSize: 9,
|
||||||
|
defaultPageSize: 9,
|
||||||
|
}}
|
||||||
|
showHeader
|
||||||
|
bordered
|
||||||
|
sticky
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
showSorterTooltip
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2708,6 +2708,14 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
|
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
|
||||||
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
|
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
|
||||||
|
|
||||||
|
"@xstate/react@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.0.tgz#888d9a6f128c70b632c18ad55f1f851f6ab092ba"
|
||||||
|
integrity sha512-KHSCfwtb8gZ7QH2luihvmKYI+0lcdHQOmGNRUxUEs4zVgaJCyd8csCEmwPsudpliLdUmyxX2pzUBojFkINpotw==
|
||||||
|
dependencies:
|
||||||
|
use-isomorphic-layout-effect "^1.0.0"
|
||||||
|
use-sync-external-store "^1.0.0"
|
||||||
|
|
||||||
"@xtuc/ieee754@^1.2.0":
|
"@xtuc/ieee754@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||||
@ -8012,6 +8020,11 @@ jest@^27.5.1:
|
|||||||
import-local "^3.0.2"
|
import-local "^3.0.2"
|
||||||
jest-cli "^27.5.1"
|
jest-cli "^27.5.1"
|
||||||
|
|
||||||
|
js-base64@^3.7.2:
|
||||||
|
version "3.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745"
|
||||||
|
integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==
|
||||||
|
|
||||||
js-cookie@^2.2.1:
|
js-cookie@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
|
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
|
||||||
@ -12392,6 +12405,16 @@ url@^0.11.0:
|
|||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
|
|
||||||
|
use-isomorphic-layout-effect@^1.0.0:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
||||||
|
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
||||||
|
|
||||||
|
use-sync-external-store@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0.tgz#d98f4a9c2e73d0f958e7e2d2c2bfb5f618cbd8fd"
|
||||||
|
integrity sha512-AFVsxg5GkFg8GDcxnl+Z0lMAz9rE8DGJCc28qnBuQF7lac57B5smLcT37aXpXIIPz75rW4g3eXHPjhHwdGskOw==
|
||||||
|
|
||||||
use@^3.1.0:
|
use@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
@ -12878,6 +12901,11 @@ xss@1.0.10:
|
|||||||
commander "^2.20.3"
|
commander "^2.20.3"
|
||||||
cssfilter "0.0.10"
|
cssfilter "0.0.10"
|
||||||
|
|
||||||
|
xstate@^4.31.0:
|
||||||
|
version "4.31.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.31.0.tgz#039cf6f865dd9e104012eb76a14df757c988ec58"
|
||||||
|
integrity sha512-UK5m6OqUsTlPuKWkfRR5cR9/Yt7sysFyEg+PVIbEH9mwHSf9zuCvWO7rRvhBq7T+3pEXLKTEMfaqmLxl9Ob1pw==
|
||||||
|
|
||||||
xtend@^4.0.0:
|
xtend@^4.0.0:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user