mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-23 17:44:27 +08:00
feat: show environments in a separate dropdown (#4717)
* feat: show environments in a separate dropdown
This commit is contained in:
parent
dbd4363ff8
commit
a30b75a2a8
@ -0,0 +1,20 @@
|
|||||||
|
.resourceAttributesFilter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: stretch;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.resource-attributes-selector {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-selector {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,17 @@
|
|||||||
|
import './ResourceAttributesFilter.styles.scss';
|
||||||
|
|
||||||
import { CloseCircleFilled } from '@ant-design/icons';
|
import { CloseCircleFilled } from '@ant-design/icons';
|
||||||
import { Button, Select, Spin } from 'antd';
|
import { Button, Select, Spin } from 'antd';
|
||||||
import useResourceAttribute, {
|
import useResourceAttribute, {
|
||||||
isResourceEmpty,
|
isResourceEmpty,
|
||||||
} from 'hooks/useResourceAttribute';
|
} from 'hooks/useResourceAttribute';
|
||||||
import { convertMetricKeyToTrace } from 'hooks/useResourceAttribute/utils';
|
import {
|
||||||
import { ReactNode, useMemo } from 'react';
|
convertMetricKeyToTrace,
|
||||||
|
getEnvironmentTagKeys,
|
||||||
|
getEnvironmentTagValues,
|
||||||
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
|
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { SelectOption } from 'types/common/select';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
@ -22,60 +29,129 @@ function ResourceAttributesFilter({
|
|||||||
handleClearAll,
|
handleClearAll,
|
||||||
handleFocus,
|
handleFocus,
|
||||||
handleChange,
|
handleChange,
|
||||||
|
handleEnvironmentChange,
|
||||||
selectedQuery,
|
selectedQuery,
|
||||||
optionsData,
|
optionsData,
|
||||||
loading,
|
loading,
|
||||||
} = useResourceAttribute();
|
} = useResourceAttribute();
|
||||||
|
|
||||||
const isEmpty = useMemo(
|
const [environments, setEnvironments] = useState<
|
||||||
() => isResourceEmpty(queries, staging, selectedQuery),
|
SelectOption<string, string>[]
|
||||||
[queries, selectedQuery, staging],
|
>([]);
|
||||||
|
|
||||||
|
const [selectedEnvironments, setSelectedEnvironments] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const queriesExcludingEnvironment = useMemo(
|
||||||
|
() =>
|
||||||
|
queries.filter(
|
||||||
|
(query) => query.tagKey !== 'resource_deployment_environment',
|
||||||
|
),
|
||||||
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const isEmpty = useMemo(
|
||||||
<SearchContainer>
|
() => isResourceEmpty(queriesExcludingEnvironment, staging, selectedQuery),
|
||||||
<div>
|
[queriesExcludingEnvironment, selectedQuery, staging],
|
||||||
{queries.map((query) => (
|
);
|
||||||
<QueryChip key={query.id} queryData={query} onClose={handleClose} />
|
|
||||||
))}
|
|
||||||
{staging.map((query, idx) => (
|
|
||||||
<QueryChipItem key={uuid()}>
|
|
||||||
{idx === 0 ? convertMetricKeyToTrace(query) : query}
|
|
||||||
</QueryChipItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
getPopupContainer={popupContainer}
|
|
||||||
placeholder={!isEmpty && 'Search and Filter based on resource attributes.'}
|
|
||||||
onChange={handleChange}
|
|
||||||
bordered={false}
|
|
||||||
value={selectedQuery as never}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
options={optionsData.options}
|
|
||||||
mode={optionsData?.mode}
|
|
||||||
showArrow={!!suffixIcon}
|
|
||||||
onClick={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onClear={handleClearAll}
|
|
||||||
suffixIcon={suffixIcon}
|
|
||||||
notFoundContent={
|
|
||||||
loading ? (
|
|
||||||
<span>
|
|
||||||
<Spin size="small" /> Loading...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
No resource attributes available to filter. Please refer docs to send
|
|
||||||
attributes.
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{queries.length || staging.length || selectedQuery.length ? (
|
useEffect(() => {
|
||||||
<Button onClick={handleClearAll} icon={<CloseCircleFilled />} type="text" />
|
const resourceDeploymentEnvironmentQuery = queries.filter(
|
||||||
) : null}
|
(query) => query.tagKey === 'resource_deployment_environment',
|
||||||
</SearchContainer>
|
);
|
||||||
|
|
||||||
|
if (resourceDeploymentEnvironmentQuery?.length > 0) {
|
||||||
|
setSelectedEnvironments(resourceDeploymentEnvironmentQuery[0].tagValue);
|
||||||
|
} else {
|
||||||
|
setSelectedEnvironments([]);
|
||||||
|
}
|
||||||
|
}, [queries]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getEnvironmentTagKeys().then((tagKeys) => {
|
||||||
|
if (tagKeys && Array.isArray(tagKeys) && tagKeys.length > 0) {
|
||||||
|
getEnvironmentTagValues().then((tagValues) => {
|
||||||
|
setEnvironments(tagValues);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="resourceAttributesFilter-container">
|
||||||
|
<div className="environment-selector">
|
||||||
|
<Select
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
key={selectedEnvironments.join('')}
|
||||||
|
showSearch
|
||||||
|
mode="multiple"
|
||||||
|
value={selectedEnvironments}
|
||||||
|
placeholder="Select Environment/s"
|
||||||
|
data-testId="resource-environment-filter"
|
||||||
|
style={{ minWidth: 200, height: 34 }}
|
||||||
|
onChange={handleEnvironmentChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
>
|
||||||
|
{environments.map((opt) => (
|
||||||
|
<Select.Option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="resource-attributes-selector">
|
||||||
|
<SearchContainer>
|
||||||
|
<div>
|
||||||
|
{queriesExcludingEnvironment.map((query) => (
|
||||||
|
<QueryChip key={query.id} queryData={query} onClose={handleClose} />
|
||||||
|
))}
|
||||||
|
{staging.map((query, idx) => (
|
||||||
|
<QueryChipItem key={uuid()}>
|
||||||
|
{idx === 0 ? convertMetricKeyToTrace(query) : query}
|
||||||
|
</QueryChipItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
placeholder={
|
||||||
|
!isEmpty && 'Search and Filter based on resource attributes.'
|
||||||
|
}
|
||||||
|
onChange={handleChange}
|
||||||
|
bordered={false}
|
||||||
|
value={selectedQuery as never}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
options={optionsData.options}
|
||||||
|
mode={optionsData?.mode}
|
||||||
|
data-testId="resource-attributes-filter"
|
||||||
|
showArrow={!!suffixIcon}
|
||||||
|
onClick={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onClear={handleClearAll}
|
||||||
|
suffixIcon={suffixIcon}
|
||||||
|
notFoundContent={
|
||||||
|
loading ? (
|
||||||
|
<span>
|
||||||
|
<Spin size="small" /> Loading...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
No resource attributes available to filter. Please refer docs to send
|
||||||
|
attributes.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{queries.length || staging.length || selectedQuery.length ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleClearAll}
|
||||||
|
icon={<CloseCircleFilled />}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</SearchContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,10 @@ function QueryChip({ queryData, onClose }: IQueryChipProps): JSX.Element {
|
|||||||
<QueryChipContainer>
|
<QueryChipContainer>
|
||||||
<QueryChipItem>{convertMetricKeyToTrace(queryData.tagKey)}</QueryChipItem>
|
<QueryChipItem>{convertMetricKeyToTrace(queryData.tagKey)}</QueryChipItem>
|
||||||
<QueryChipItem>{queryData.operator}</QueryChipItem>
|
<QueryChipItem>{queryData.operator}</QueryChipItem>
|
||||||
<QueryChipItem closable onClose={onCloseHandler}>
|
<QueryChipItem
|
||||||
|
closable={queryData.tagKey !== 'resource_deployment_environment'}
|
||||||
|
onClose={onCloseHandler}
|
||||||
|
>
|
||||||
{queryData.tagValue.join(', ')}
|
{queryData.tagValue.join(', ')}
|
||||||
</QueryChipItem>
|
</QueryChipItem>
|
||||||
</QueryChipContainer>
|
</QueryChipContainer>
|
||||||
|
@ -7,9 +7,10 @@ export const SearchContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
padding: 0.2rem;
|
padding: 0 0.2rem;
|
||||||
margin: 1rem 0;
|
border: 1px solid #454c58;
|
||||||
border: 1px solid #ccc5;
|
box-sizing: border-box;
|
||||||
|
border-radius: 3px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const QueryChipContainer = styled.span`
|
export const QueryChipContainer = styled.span`
|
||||||
|
@ -52,6 +52,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
|||||||
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
|
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
|
||||||
: '',
|
: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
setQueries(queries);
|
setQueries(queries);
|
||||||
},
|
},
|
||||||
[pathname],
|
[pathname],
|
||||||
@ -62,12 +63,14 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
|||||||
onSelectTagKey: () => {
|
onSelectTagKey: () => {
|
||||||
handleLoading(true);
|
handleLoading(true);
|
||||||
GetTagKeys()
|
GetTagKeys()
|
||||||
.then((tagKeys) =>
|
.then((tagKeys) => {
|
||||||
|
const options = mappingWithRoutesAndKeys(pathname, tagKeys);
|
||||||
|
|
||||||
setOptionsData({
|
setOptionsData({
|
||||||
options: mappingWithRoutesAndKeys(pathname, tagKeys),
|
options,
|
||||||
mode: undefined,
|
mode: undefined,
|
||||||
}),
|
});
|
||||||
)
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
handleLoading(false);
|
handleLoading(false);
|
||||||
});
|
});
|
||||||
@ -96,6 +99,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const generatedQuery = createQuery([...staging, selectedQuery]);
|
const generatedQuery = createQuery([...staging, selectedQuery]);
|
||||||
|
|
||||||
if (generatedQuery) {
|
if (generatedQuery) {
|
||||||
dispatchQueries([...queries, generatedQuery]);
|
dispatchQueries([...queries, generatedQuery]);
|
||||||
}
|
}
|
||||||
@ -127,6 +131,29 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
|||||||
[optionsData.mode, send],
|
[optionsData.mode, send],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleEnvironmentChange = useCallback(
|
||||||
|
(environments: string[]): void => {
|
||||||
|
const staging = ['resource_deployment_environment', 'IN'];
|
||||||
|
|
||||||
|
const queriesCopy = queries.filter(
|
||||||
|
(query) => query.tagKey !== 'resource_deployment_environment',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (environments && Array.isArray(environments) && environments.length > 0) {
|
||||||
|
const generatedQuery = createQuery([...staging, environments]);
|
||||||
|
|
||||||
|
if (generatedQuery) {
|
||||||
|
dispatchQueries([...queriesCopy, generatedQuery]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatchQueries([...queriesCopy]);
|
||||||
|
}
|
||||||
|
|
||||||
|
send('RESET');
|
||||||
|
},
|
||||||
|
[dispatchQueries, queries, send],
|
||||||
|
);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = useCallback(
|
||||||
(id: string): void => {
|
(id: string): void => {
|
||||||
dispatchQueries(queries.filter((queryData) => queryData.id !== id));
|
dispatchQueries(queries.filter((queryData) => queryData.id !== id));
|
||||||
@ -159,12 +186,14 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
|||||||
handleFocus,
|
handleFocus,
|
||||||
loading,
|
loading,
|
||||||
handleChange,
|
handleChange,
|
||||||
|
handleEnvironmentChange,
|
||||||
selectedQuery,
|
selectedQuery,
|
||||||
optionsData,
|
optionsData,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
handleBlur,
|
handleBlur,
|
||||||
handleChange,
|
handleChange,
|
||||||
|
handleEnvironmentChange,
|
||||||
handleClearAll,
|
handleClearAll,
|
||||||
handleClose,
|
handleClose,
|
||||||
handleFocus,
|
handleFocus,
|
||||||
|
@ -28,4 +28,5 @@ export interface IResourceAttributeProps {
|
|||||||
handleChange: (value: string) => void;
|
handleChange: (value: string) => void;
|
||||||
selectedQuery: string[];
|
selectedQuery: string[];
|
||||||
optionsData: OptionsData;
|
optionsData: OptionsData;
|
||||||
|
handleEnvironmentChange: (environments: string[]) => void;
|
||||||
}
|
}
|
||||||
|
@ -109,12 +109,43 @@ export const GetTagKeys = async (): Promise<IOption[]> => {
|
|||||||
if (!payload || !payload?.data) {
|
if (!payload || !payload?.data) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
return payload.data
|
||||||
|
.filter((tagKey: string) => tagKey !== 'resource_deployment_environment')
|
||||||
|
.map((tagKey: string) => ({
|
||||||
|
label: convertMetricKeyToTrace(tagKey),
|
||||||
|
value: tagKey,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEnvironmentTagKeys = async (): Promise<IOption[]> => {
|
||||||
|
const { payload } = await getResourceAttributesTagKeys({
|
||||||
|
metricName: 'signoz_calls_total',
|
||||||
|
match: 'resource_deployment_environment',
|
||||||
|
});
|
||||||
|
if (!payload || !payload?.data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return payload.data.map((tagKey: string) => ({
|
return payload.data.map((tagKey: string) => ({
|
||||||
label: convertMetricKeyToTrace(tagKey),
|
label: convertMetricKeyToTrace(tagKey),
|
||||||
value: tagKey,
|
value: tagKey,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEnvironmentTagValues = async (): Promise<IOption[]> => {
|
||||||
|
const { payload } = await getResourceAttributesTagValues({
|
||||||
|
tagKey: 'resource_deployment_environment',
|
||||||
|
metricName: 'signoz_calls_total',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!payload || !payload?.data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return payload.data.map((tagValue: string) => ({
|
||||||
|
label: tagValue,
|
||||||
|
value: tagValue,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const GetTagValues = async (tagKey: string): Promise<IOption[]> => {
|
export const GetTagValues = async (tagKey: string): Promise<IOption[]> => {
|
||||||
const { payload } = await getResourceAttributesTagValues({
|
const { payload } = await getResourceAttributesTagValues({
|
||||||
tagKey,
|
tagKey,
|
||||||
@ -132,6 +163,23 @@ export const GetTagValues = async (tagKey: string): Promise<IOption[]> => {
|
|||||||
|
|
||||||
export const createQuery = (
|
export const createQuery = (
|
||||||
selectedItems: Array<string | string[]> = [],
|
selectedItems: Array<string | string[]> = [],
|
||||||
|
): IResourceAttribute | null => {
|
||||||
|
console.log('selectedItems', selectedItems);
|
||||||
|
|
||||||
|
if (selectedItems.length === 3) {
|
||||||
|
return {
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
tagKey: selectedItems[0] as string,
|
||||||
|
operator: selectedItems[1] as string,
|
||||||
|
tagValue: selectedItems[2] as string[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateQuery = (
|
||||||
|
queryKey: string,
|
||||||
|
selectedItems: Array<string | string[]> = [],
|
||||||
): IResourceAttribute | null => {
|
): IResourceAttribute | null => {
|
||||||
if (selectedItems.length === 3) {
|
if (selectedItems.length === 3) {
|
||||||
return {
|
return {
|
||||||
|
@ -6,9 +6,11 @@ describe('Services', () => {
|
|||||||
test('Should render the component', () => {
|
test('Should render the component', () => {
|
||||||
render(<Metrics />);
|
render(<Metrics />);
|
||||||
|
|
||||||
const inputBox = screen.getByRole('combobox');
|
const inputBox = screen.getByTestId('resource-attributes-filter');
|
||||||
expect(inputBox).toBeInTheDocument();
|
expect(inputBox).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('resource-environment-filter')).toBeInTheDocument();
|
||||||
|
|
||||||
const application = screen.getByRole('columnheader', {
|
const application = screen.getByRole('columnheader', {
|
||||||
name: /application search/i,
|
name: /application search/i,
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user