Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,60 +1,24 @@
import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
import {t} from 'sentry/locale';
import type {TagCollection} from 'sentry/types/group';
import type {FieldDefinition} from 'sentry/utils/fields';
import {FieldKind} from 'sentry/utils/fields';
import {AUTOMATION_FILTER_KEYS} from 'sentry/views/automations/constants';
import {useAutomationFilterKeys} from 'sentry/views/automations/utils/useAutomationFilterKeys';

type AutomationSearchProps = {
initialQuery: string;
onSearch: (query: string) => void;
};

function getAutomationFilterKeyDefinition(filterKey: string): FieldDefinition | null {
if (
AUTOMATION_FILTER_KEYS.hasOwnProperty(filterKey) &&
AUTOMATION_FILTER_KEYS[filterKey]
) {
const {description, valueType, keywords, values} = AUTOMATION_FILTER_KEYS[filterKey];

return {
kind: FieldKind.FIELD,
desc: description,
valueType,
keywords,
values,
};
}

return null;
}

const FILTER_KEYS: TagCollection = Object.fromEntries(
Object.keys(AUTOMATION_FILTER_KEYS).map(key => {
const {values} = AUTOMATION_FILTER_KEYS[key] ?? {};

return [
key,
{
key,
name: key,
predefined: values !== undefined,
values,
},
];
})
);

export function AutomationSearch({initialQuery, onSearch}: AutomationSearchProps) {
const {filterKeys, getFieldDefinition} = useAutomationFilterKeys();

return (
<SearchQueryBuilder
initialQuery={initialQuery}
placeholder={t('Search for alerts')}
onSearch={onSearch}
filterKeys={FILTER_KEYS}
filterKeys={filterKeys}
getTagValues={() => Promise.resolve([])}
searchSource="automations-list"
fieldDefinitionGetter={getAutomationFilterKeyDefinition}
fieldDefinitionGetter={getFieldDefinition}
disallowUnsupportedFilters
disallowWildcard
disallowLogicalOperators
Expand Down
26 changes: 0 additions & 26 deletions static/app/views/automations/constants.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1 @@
import {FieldValueType} from 'sentry/utils/fields';
import {ActionType} from 'sentry/views/alerts/rules/metric/types';

export const AUTOMATION_LIST_PAGE_LIMIT = 20;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! I like how this stuff is living a lot closer to where it's used now -- should we do that with AUTOMATION_LIST_PAGE_LIMIT as well? 🤔


const ACTION_TYPE_VALUES = Object.values(ActionType).sort();

export const AUTOMATION_FILTER_KEYS: Record<
string,
{
description: string;
valueType: FieldValueType;
keywords?: string[];
values?: string[];
}
> = {
name: {
description: 'Name of the automation (exact match).',
valueType: FieldValueType.STRING,
keywords: ['name'],
},
action: {
description: 'Action triggered by the automation.',
valueType: FieldValueType.STRING,
values: ACTION_TYPE_VALUES,
},
};
23 changes: 23 additions & 0 deletions static/app/views/automations/list.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ describe('AutomationsList', () => {
url: '/organizations/org-slug/prompts-activity/',
body: {},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/members/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/workflows/',
body: [AutomationFixture({name: 'Automation 1'})],
Expand Down Expand Up @@ -189,6 +193,25 @@ describe('AutomationsList', () => {
await screen.findByText('Slack Automation');
expect(mockAutomationActionSlack).toHaveBeenCalled();
});

it('can filter by created_by', async () => {
const mockAutomationCreatedBy = MockApiClient.addMockResponse({
url: '/organizations/org-slug/workflows/',
body: [AutomationFixture({name: 'My Automation'})],
match: [MockApiClient.matchQuery({query: 'created_by:me'})],
});

render(<AutomationsList />, {organization});
await screen.findByText('Automation 1');

// Click through menus to select created_by:me
await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));
await userEvent.click(await screen.findByRole('option', {name: 'created_by'}));
await userEvent.click(await screen.findByRole('option', {name: 'me'}));

await screen.findByText('My Automation');
expect(mockAutomationCreatedBy).toHaveBeenCalled();
});
});

describe('bulk actions', () => {
Expand Down
108 changes: 108 additions & 0 deletions static/app/views/automations/utils/useAutomationFilterKeys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {useCallback, useMemo} from 'react';

import {ItemType, type SearchGroup} from 'sentry/components/searchBar/types';
import {escapeTagValue} from 'sentry/components/searchBar/utils';
import type {FieldDefinitionGetter} from 'sentry/components/searchQueryBuilder/types';
import {IconStar, IconUser} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {TagCollection} from 'sentry/types/group';
import {FieldKind, FieldValueType, type FieldDefinition} from 'sentry/utils/fields';
import {getUsername} from 'sentry/utils/membersAndTeams/userUtils';
import {useMembers} from 'sentry/utils/useMembers';
import {ActionType} from 'sentry/views/alerts/rules/metric/types';

const ACTION_TYPE_VALUES = Object.values(ActionType).sort();

const AUTOMATION_FILTER_KEYS: Record<
string,
{
fieldDefinition: FieldDefinition;
predefined?: boolean;
}
> = {
name: {
fieldDefinition: {
desc: 'Name of the Alert.',
kind: FieldKind.FIELD,
valueType: FieldValueType.STRING,
keywords: ['name'],
},
},
action: {
predefined: true,
fieldDefinition: {
desc: 'Action triggered by the Alert.',
kind: FieldKind.FIELD,
valueType: FieldValueType.STRING,
values: ACTION_TYPE_VALUES,
},
},
created_by: {
predefined: true,
fieldDefinition: {
desc: 'User who created the Alert.',
kind: FieldKind.FIELD,
valueType: FieldValueType.STRING,
allowWildcard: false,
keywords: ['creator', 'author'],
},
},
};

const convertToSearchItem = (value: string) => {
const escapedValue = escapeTagValue(value);
return {
value: escapedValue,
desc: value,
type: ItemType.TAG_VALUE,
};
};

export function useAutomationFilterKeys(): {
filterKeys: TagCollection;
getFieldDefinition: FieldDefinitionGetter;
} {
const {members} = useMembers();

const createdByValues: SearchGroup[] = useMemo(() => {
const usernames = members.map(getUsername);

return [
{
title: t('Suggested Values'),
type: 'header',
icon: <IconStar size="xs" />,
children: [convertToSearchItem('me')],
},
{
title: t('All Values'),
type: 'header',
icon: <IconUser size="xs" />,
children: usernames.map(convertToSearchItem),
},
];
}, [members]);

const filterKeys = useMemo(() => {
const entries = Object.entries(AUTOMATION_FILTER_KEYS).map(([key, config]) => [
key,
{
key,
name: key,
predefined: config.predefined,
values: key === 'created_by' ? createdByValues : undefined,
},
]);

return Object.fromEntries(entries);
}, [createdByValues]);

const getFieldDefinition = useCallback<FieldDefinitionGetter>((key: string) => {
return AUTOMATION_FILTER_KEYS[key]?.fieldDefinition ?? null;
}, []);

return {
filterKeys,
getFieldDefinition,
};
}
11 changes: 11 additions & 0 deletions static/app/views/detectors/detectorViewContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import {useEffect} from 'react';
import {Outlet} from 'react-router-dom';

import {fetchOrgMembers} from 'sentry/actionCreators/members';
import {PageFiltersContainer} from 'sentry/components/pageFilters/container';
import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate';
import {useApi} from 'sentry/utils/useApi';
import {useOrganization} from 'sentry/utils/useOrganization';

export default function DetectorViewContainer() {
useWorkflowEngineFeatureGate({redirect: true});

const api = useApi();
const organization = useOrganization();

useEffect(() => {
fetchOrgMembers(api, organization.slug);
}, [api, organization.slug]);

return (
<PageFiltersContainer>
<Outlet />
Expand Down
Loading