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 { 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>
); );
} }

View File

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

View File

@ -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`

View File

@ -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,

View File

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

View File

@ -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 {

View File

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