Skip to content
135 changes: 41 additions & 94 deletions static/app/views/automations/hooks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ import type {
} from 'sentry/types/workflowEngine/dataConditions';
import {apiOptions} from 'sentry/utils/api/apiOptions';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import type {
ApiQueryKey,
UseApiQueryOptions,
UseMutationOptions,
} from 'sentry/utils/queryClient';
import type {ApiQueryKey, UseMutationOptions} from 'sentry/utils/queryClient';
import {
setApiQueryData,
useApiQuery,
Expand All @@ -30,73 +26,48 @@ import type {RequestError} from 'sentry/utils/requestError/requestError';
import {useApi} from 'sentry/utils/useApi';
import {useOrganization} from 'sentry/utils/useOrganization';

export const makeAutomationsQueryKey = ({
orgSlug,
query,
sortBy,
priorityDetector,
ids,
limit,
cursor,
projects,
detector,
}: {
orgSlug: string;
cursor?: string;
detector?: string[];
ids?: string[];
limit?: number;
priorityDetector?: string;
projects?: number[];
query?: string;
sortBy?: string;
}): ApiQueryKey => [
getApiUrl('/organizations/$organizationIdOrSlug/workflows/', {
path: {organizationIdOrSlug: orgSlug},
}),
{
query: {
export const automationsApiOptions = (
organization: Organization,
options?: {
cursor?: string;
Comment thread
TkDodo marked this conversation as resolved.
detector?: string[];
ids?: string[];
limit?: number;
priorityDetector?: string;
projects?: number[];
query?: string;
sortBy?: string;
}
) => {
const query = options
? {
query: options.query,
sortBy: options.sortBy,
priorityDetector: options.priorityDetector,
id: options.ids,
per_page: options.limit,
cursor: options.cursor,
project: options.projects,
detector: options.detector,
}
: undefined;

return queryOptions({
...apiOptions.as<Automation[]>()('/organizations/$organizationIdOrSlug/workflows/', {
path: {organizationIdOrSlug: organization.slug},
query,
sortBy,
priorityDetector,
id: ids,
per_page: limit,
cursor,
project: projects,
detector,
},
},
];
staleTime: 0,
}),
retry: false,
});
};
Comment thread
TkDodo marked this conversation as resolved.

const makeAutomationQueryKey = (orgSlug: string, automationId: string): ApiQueryKey => [
getApiUrl('/organizations/$organizationIdOrSlug/workflows/$workflowId/', {
path: {organizationIdOrSlug: orgSlug, workflowId: automationId},
}),
];

interface UseAutomationsQueryOptions {
cursor?: string;
detector?: string[];
ids?: string[];
limit?: number;
priorityDetector?: string;
projects?: number[];
query?: string;
sortBy?: string;
}
export function useAutomationsQuery(
options: UseAutomationsQueryOptions = {},
useApiQueryOptions: Partial<UseApiQueryOptions<Automation[]>> = {}
) {
const {slug: orgSlug} = useOrganization();

return useApiQuery<Automation[]>(makeAutomationsQueryKey({orgSlug, ...options}), {
staleTime: 0,
retry: false,
...useApiQueryOptions,
});
}

export function useAutomationQuery(automationId: string) {
const {slug} = useOrganization();

Expand Down Expand Up @@ -186,11 +157,7 @@ export function useCreateAutomation() {
),
onSuccess: _ => {
queryClient.invalidateQueries({
queryKey: [
getApiUrl('/organizations/$organizationIdOrSlug/workflows/', {
path: {organizationIdOrSlug: org.slug},
}),
],
queryKey: automationsApiOptions(org).queryKey,
});
},
onError: _ => {
Expand All @@ -216,11 +183,7 @@ export function useDeleteAutomationMutation() {
),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [
getApiUrl('/organizations/$organizationIdOrSlug/workflows/', {
path: {organizationIdOrSlug: org.slug},
}),
],
queryKey: automationsApiOptions(org).queryKey,
});
addSuccessMessage(t('Alert deleted'));
},
Expand Down Expand Up @@ -258,11 +221,7 @@ export function useDeleteAutomationsMutation() {
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [
getApiUrl('/organizations/$organizationIdOrSlug/workflows/', {
path: {organizationIdOrSlug: org.slug},
}),
],
queryKey: automationsApiOptions(org).queryKey,
});
addSuccessMessage(t('Alerts deleted'));
},
Expand Down Expand Up @@ -305,11 +264,7 @@ export function useUpdateAutomation() {
);
// Invalidate list query
queryClient.invalidateQueries({
queryKey: [
getApiUrl('/organizations/$organizationIdOrSlug/workflows/', {
path: {organizationIdOrSlug: org.slug},
}),
],
queryKey: automationsApiOptions(org).queryKey,
});
},
onError: _ => {
Expand Down Expand Up @@ -347,11 +302,7 @@ export function useUpdateAutomationsMutation() {
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: [
getApiUrl('/organizations/$organizationIdOrSlug/workflows/', {
path: {organizationIdOrSlug: org.slug},
}),
],
queryKey: automationsApiOptions(org).queryKey,
});
addSuccessMessage(variables.enabled ? t('Alerts enabled') : t('Alerts disabled'));
},
Expand Down Expand Up @@ -388,11 +339,7 @@ export function useSendTestNotification(
...options,
onSuccess: (data, variables, onMutateResult, context) => {
queryClient.invalidateQueries({
queryKey: [
getApiUrl('/organizations/$organizationIdOrSlug/workflows/', {
path: {organizationIdOrSlug: org.slug},
}),
],
queryKey: automationsApiOptions(org).queryKey,
});
addSuccessMessage(
tn('Notification fired!', 'Notifications sent!', variables.length)
Expand Down
36 changes: 16 additions & 20 deletions static/app/views/automations/list.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useCallback} from 'react';
import {useQuery} from '@tanstack/react-query';

import {LinkButton} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
Expand All @@ -10,6 +11,7 @@ import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle';
import {WorkflowEngineListLayout as ListLayout} from 'sentry/components/workflowEngine/layout/list';
import {IconAdd} from 'sentry/icons';
import {t} from 'sentry/locale';
import {selectJsonWithHeaders} from 'sentry/utils/api/apiOptions';
import {parseLinkHeader} from 'sentry/utils/parseLinkHeader';
import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
Expand All @@ -21,11 +23,12 @@ import {AutomationFeedbackButton} from 'sentry/views/automations/components/auto
import {AutomationListTable} from 'sentry/views/automations/components/automationListTable';
import {AutomationSearch} from 'sentry/views/automations/components/automationListTable/search';
import {AUTOMATION_LIST_PAGE_LIMIT} from 'sentry/views/automations/constants';
import {useAutomationsQuery} from 'sentry/views/automations/hooks';
import {automationsApiOptions} from 'sentry/views/automations/hooks';
import {makeAutomationCreatePathname} from 'sentry/views/automations/pathnames';
import {AlertsRedirectNotice} from 'sentry/views/detectors/list/common/alertsRedirectNotice';

export default function AutomationsList() {
const organization = useOrganization();
const location = useLocation();
const navigate = useNavigate();
const {selection, isReady} = usePageFilters();
Expand All @@ -43,30 +46,23 @@ export default function AutomationsList() {
});
const sort = sorts[0] ?? {kind: 'desc', field: 'lastTriggered'};

const {
data: automations,
isLoading,
isError,
isSuccess,
getResponseHeader,
} = useAutomationsQuery(
{
cursor,
const {data, isLoading, isError, isSuccess} = useQuery({
...automationsApiOptions(organization, {
query,
sortBy: sort ? `${sort?.kind === 'asc' ? '' : '-'}${sort?.field}` : undefined,
projects: selection.projects,
limit: AUTOMATION_LIST_PAGE_LIMIT,
},
{enabled: isReady}
);
cursor,
}),
select: selectJsonWithHeaders,
enabled: isReady,
});

const hits = getResponseHeader?.('X-Hits') || '';
const hitsInt = hits ? parseInt(hits, 10) || 0 : 0;
const automations = data?.json;
const hits = data?.headers['X-Hits'] ?? 0;
// If maxHits is not set, we assume there is no max
const maxHits = getResponseHeader?.('X-Max-Hits') || '';
const maxHitsInt = maxHits ? parseInt(maxHits, 10) || Infinity : Infinity;

const pageLinks = getResponseHeader?.('Link');
const maxHits = data?.headers['X-Max-Hits'] ?? Infinity;
const pageLinks = data?.headers.Link;

const allResultsVisible = useCallback(() => {
if (!pageLinks) {
Expand Down Expand Up @@ -102,7 +98,7 @@ export default function AutomationsList() {
isError={isError}
isSuccess={isSuccess}
sort={sort}
queryCount={hitsInt > maxHitsInt ? `${maxHits}+` : hits}
queryCount={hits > maxHits ? `${maxHits}+` : `${hits}`}
allResultsVisible={allResultsVisible()}
/>
</VisuallyCompleteWithData>
Comment thread
TkDodo marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {DrawerHeader} from 'sentry/components/globalDrawer/components';
import {DetailSection} from 'sentry/components/workflowEngine/ui/detailSection';
import {t} from 'sentry/locale';
import type {Automation} from 'sentry/types/workflowEngine/automations';
import {getApiQueryData, setApiQueryData, useQueryClient} from 'sentry/utils/queryClient';
import {useQueryClient} from 'sentry/utils/queryClient';
import {useOrganization} from 'sentry/utils/useOrganization';
import {AutomationSearch} from 'sentry/views/automations/components/automationListTable/search';
import {makeAutomationsQueryKey} from 'sentry/views/automations/hooks';
import {automationsApiOptions} from 'sentry/views/automations/hooks';
import {ConnectedAutomationsList} from 'sentry/views/detectors/components/connectedAutomationList';

function ConnectedAutomations({
Expand Down Expand Up @@ -81,13 +81,11 @@ export function ConnectAutomationsDrawer({

const toggleConnected = ({automation}: {automation: Automation}) => {
const oldAutomationsData =
getApiQueryData<Automation[]>(
queryClient,
makeAutomationsQueryKey({
orgSlug: organization.slug,
queryClient.getQueryData(
automationsApiOptions(organization, {
ids: localWorkflowIds,
})
) ?? [];
}).queryKey
)?.json ?? [];

const newAutomations = (
oldAutomationsData.some(a => a.id === automation.id)
Expand All @@ -96,13 +94,9 @@ export function ConnectAutomationsDrawer({
).sort((a, b) => a.id.localeCompare(b.id));
const newWorkflowIds = newAutomations.map(a => a.id);

setApiQueryData<Automation[]>(
queryClient,
makeAutomationsQueryKey({
orgSlug: organization.slug,
ids: newWorkflowIds,
}),
newAutomations
queryClient.setQueryData(
automationsApiOptions(organization, {ids: newWorkflowIds}).queryKey,
old => ({headers: old?.headers ?? {}, json: newAutomations})
);

setLocalWorkflowIds(newWorkflowIds);
Expand Down
33 changes: 16 additions & 17 deletions static/app/views/detectors/components/connectedAutomationList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Fragment, useMemo} from 'react';
import styled from '@emotion/styled';
import {useQuery} from '@tanstack/react-query';

import {Button} from '@sentry/scraps/button';

Expand All @@ -14,8 +15,10 @@ import {TimeAgoCell} from 'sentry/components/workflowEngine/gridCell/timeAgoCell
import {t, tct} from 'sentry/locale';
import type {Automation} from 'sentry/types/workflowEngine/automations';
import type {Detector} from 'sentry/types/workflowEngine/detectors';
import {selectJsonWithHeaders} from 'sentry/utils/api/apiOptions';
import {parseCursor} from 'sentry/utils/cursor';
import {useAutomationsQuery} from 'sentry/views/automations/hooks';
import {useOrganization} from 'sentry/utils/useOrganization';
import {automationsApiOptions} from 'sentry/views/automations/hooks';
import {getAutomationActions} from 'sentry/views/automations/hooks/utils';

const DEFAULT_AUTOMATIONS_PER_PAGE = 10;
Expand Down Expand Up @@ -72,29 +75,25 @@ export function ConnectedAutomationsList({
openInNewTab,
...props
}: Props) {
const organization = useOrganization();
const canEdit = Boolean(
connectedAutomationIds && typeof toggleConnected === 'function'
);

const {
data: automations,
isLoading,
isError,
isSuccess,
getResponseHeader,
} = useAutomationsQuery(
{
const {data, isLoading, isError, isSuccess} = useQuery({
...automationsApiOptions(organization, {
ids: automationIds ?? undefined,
limit: limit ?? undefined,
cursor,
query,
},
{enabled: automationIds === null || automationIds.length > 0}
);
}),
select: selectJsonWithHeaders,
enabled: automationIds === null || automationIds.length > 0,
});

const pageLinks = getResponseHeader?.('Link');
const totalCount = getResponseHeader?.('X-Hits');
const totalCountInt = totalCount ? parseInt(totalCount, 10) : 0;
const automations = data?.json;
const pageLinks = data?.headers.Link;
const totalCountInt = data?.headers['X-Hits'] ?? 0;

const paginationCaption = useMemo(() => {
if (!automations || automations.length === 0 || isLoading || limit === null) {
Expand Down Expand Up @@ -137,12 +136,12 @@ export function ConnectedAutomationsList({
/>
)}
{isError && <LoadingError />}
{((isSuccess && automations.length === 0) ||
{((isSuccess && automations?.length === 0) ||
(automationIds !== null && automationIds.length === 0)) && (
<SimpleTable.Empty>{emptyMessage}</SimpleTable.Empty>
)}
{isSuccess &&
automations.map(automation => (
automations?.map(automation => (
<SimpleTable.Row
key={automation.id}
variant={automation.enabled ? 'default' : 'faded'}
Expand Down
Loading
Loading