mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 22:09:05 +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
|
||||
build
|
||||
*.typegen.ts
|
||||
|
@ -31,6 +31,7 @@
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@welldone-software/why-did-you-render": "^6.2.1",
|
||||
"@xstate/react": "^3.0.0",
|
||||
"antd": "4.19.2",
|
||||
"axios": "^0.21.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
@ -58,6 +59,7 @@
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"i18next-http-backend": "^1.3.2",
|
||||
"jest": "^27.5.1",
|
||||
"js-base64": "^3.7.2",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@ -85,7 +87,8 @@
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "^5.23.0",
|
||||
"webpack-dev-server": "^4.3.1"
|
||||
"webpack-dev-server": "^4.3.1",
|
||||
"xstate": "^4.31.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"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 */
|
||||
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 { AxiosError } from 'axios';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@ -25,6 +27,11 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
|
||||
const [filteredDashboards, setFilteredDashboards] = useState<Dashboard[]>();
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredDashboards(dashboards);
|
||||
}, [dashboards]);
|
||||
const [newDashboardState, setNewDashboardState] = useState({
|
||||
loading: 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,
|
||||
description: e.data.description || '',
|
||||
id: e.uuid,
|
||||
@ -138,47 +145,52 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize: 9,
|
||||
defaultPageSize: 9,
|
||||
}}
|
||||
showHeader
|
||||
bordered
|
||||
sticky
|
||||
loading={loading}
|
||||
title={(): JSX.Element => {
|
||||
return (
|
||||
<Row justify="space-between">
|
||||
<Typography>Dashboard List</Typography>
|
||||
<Card>
|
||||
<Row justify="space-between">
|
||||
<Typography>Dashboard List</Typography>
|
||||
|
||||
<ButtonContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to create dashboards`,
|
||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||
}}
|
||||
/>
|
||||
<ButtonContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to create dashboards`,
|
||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewDashboardButton
|
||||
onClick={onNewDashboardHandler}
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
loading={newDashboardState.loading}
|
||||
danger={newDashboardState.error}
|
||||
>
|
||||
{getText()}
|
||||
</NewDashboardButton>
|
||||
</ButtonContainer>
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
showSorterTooltip
|
||||
/>
|
||||
</TableContainer>
|
||||
<NewDashboardButton
|
||||
onClick={onNewDashboardHandler}
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
loading={newDashboardState.loading}
|
||||
danger={newDashboardState.error}
|
||||
>
|
||||
{getText()}
|
||||
</NewDashboardButton>
|
||||
</ButtonContainer>
|
||||
</Row>
|
||||
{!loading && (
|
||||
<SearchFilter
|
||||
searchData={dashboards}
|
||||
filterDashboards={setFilteredDashboards}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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"
|
||||
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":
|
||||
version "1.2.0"
|
||||
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"
|
||||
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:
|
||||
version "2.2.1"
|
||||
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"
|
||||
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:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
@ -12878,6 +12901,11 @@ xss@1.0.10:
|
||||
commander "^2.20.3"
|
||||
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:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
|
Loading…
x
Reference in New Issue
Block a user