feat: show environments in a separate dropdown (#4717)

* feat: show environments in a separate dropdown
This commit is contained in:
Yunus M 2024-03-27 18:46:05 +05:30 committed by GitHub
parent dbd4363ff8
commit a30b75a2a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 237 additions and 57 deletions

View File

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

View File

@ -1,10 +1,17 @@
import './ResourceAttributesFilter.styles.scss';
import { CloseCircleFilled } from '@ant-design/icons';
import { Button, Select, Spin } from 'antd';
import useResourceAttribute, {
isResourceEmpty,
} from 'hooks/useResourceAttribute';
import { convertMetricKeyToTrace } from 'hooks/useResourceAttribute/utils';
import { ReactNode, useMemo } from 'react';
import {
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 { v4 as uuid } from 'uuid';
@ -22,60 +29,129 @@ function ResourceAttributesFilter({
handleClearAll,
handleFocus,
handleChange,
handleEnvironmentChange,
selectedQuery,
optionsData,
loading,
} = useResourceAttribute();
const isEmpty = useMemo(
() => isResourceEmpty(queries, staging, selectedQuery),
[queries, selectedQuery, staging],
const [environments, setEnvironments] = useState<
SelectOption<string, string>[]
>([]);
const [selectedEnvironments, setSelectedEnvironments] = useState<string[]>([]);
const queriesExcludingEnvironment = useMemo(
() =>
queries.filter(
(query) => query.tagKey !== 'resource_deployment_environment',
),
[queries],
);
return (
<SearchContainer>
<div>
{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>
)
}
/>
const isEmpty = useMemo(
() => isResourceEmpty(queriesExcludingEnvironment, staging, selectedQuery),
[queriesExcludingEnvironment, selectedQuery, staging],
);
{queries.length || staging.length || selectedQuery.length ? (
<Button onClick={handleClearAll} icon={<CloseCircleFilled />} type="text" />
) : null}
</SearchContainer>
useEffect(() => {
const resourceDeploymentEnvironmentQuery = queries.filter(
(query) => query.tagKey === 'resource_deployment_environment',
);
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>
);
}

View File

@ -12,7 +12,10 @@ function QueryChip({ queryData, onClose }: IQueryChipProps): JSX.Element {
<QueryChipContainer>
<QueryChipItem>{convertMetricKeyToTrace(queryData.tagKey)}</QueryChipItem>
<QueryChipItem>{queryData.operator}</QueryChipItem>
<QueryChipItem closable onClose={onCloseHandler}>
<QueryChipItem
closable={queryData.tagKey !== 'resource_deployment_environment'}
onClose={onCloseHandler}
>
{queryData.tagValue.join(', ')}
</QueryChipItem>
</QueryChipContainer>

View File

@ -7,9 +7,10 @@ export const SearchContainer = styled.div`
display: flex;
align-items: center;
gap: 0.2rem;
padding: 0.2rem;
margin: 1rem 0;
border: 1px solid #ccc5;
padding: 0 0.2rem;
border: 1px solid #454c58;
box-sizing: border-box;
border-radius: 3px;
`;
export const QueryChipContainer = styled.span`

View File

@ -52,6 +52,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
: '',
});
setQueries(queries);
},
[pathname],
@ -62,12 +63,14 @@ function ResourceProvider({ children }: Props): JSX.Element {
onSelectTagKey: () => {
handleLoading(true);
GetTagKeys()
.then((tagKeys) =>
.then((tagKeys) => {
const options = mappingWithRoutesAndKeys(pathname, tagKeys);
setOptionsData({
options: mappingWithRoutesAndKeys(pathname, tagKeys),
options,
mode: undefined,
}),
)
});
})
.finally(() => {
handleLoading(false);
});
@ -96,6 +99,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
}
const generatedQuery = createQuery([...staging, selectedQuery]);
if (generatedQuery) {
dispatchQueries([...queries, generatedQuery]);
}
@ -127,6 +131,29 @@ function ResourceProvider({ children }: Props): JSX.Element {
[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(
(id: string): void => {
dispatchQueries(queries.filter((queryData) => queryData.id !== id));
@ -159,12 +186,14 @@ function ResourceProvider({ children }: Props): JSX.Element {
handleFocus,
loading,
handleChange,
handleEnvironmentChange,
selectedQuery,
optionsData,
}),
[
handleBlur,
handleChange,
handleEnvironmentChange,
handleClearAll,
handleClose,
handleFocus,

View File

@ -28,4 +28,5 @@ export interface IResourceAttributeProps {
handleChange: (value: string) => void;
selectedQuery: string[];
optionsData: OptionsData;
handleEnvironmentChange: (environments: string[]) => void;
}

View File

@ -109,12 +109,43 @@ export const GetTagKeys = async (): Promise<IOption[]> => {
if (!payload || !payload?.data) {
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) => ({
label: convertMetricKeyToTrace(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[]> => {
const { payload } = await getResourceAttributesTagValues({
tagKey,
@ -132,6 +163,23 @@ export const GetTagValues = async (tagKey: string): Promise<IOption[]> => {
export const createQuery = (
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 => {
if (selectedItems.length === 3) {
return {

View File

@ -6,9 +6,11 @@ describe('Services', () => {
test('Should render the component', () => {
render(<Metrics />);
const inputBox = screen.getByRole('combobox');
const inputBox = screen.getByTestId('resource-attributes-filter');
expect(inputBox).toBeInTheDocument();
expect(screen.getByTestId('resource-environment-filter')).toBeInTheDocument();
const application = screen.getByRole('columnheader', {
name: /application search/i,
});