Update subscription query with Event types only and improve Event picking (#3357)

This commit is contained in:
Bartłomiej Wiaduch 2023-03-23 16:31:04 +01:00 committed by GitHub
parent b36b4859f1
commit 5dabfc315f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 191 additions and 52 deletions

View file

@ -27,6 +27,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Migrate to `es2020` for TypeScript - #3386 by @zaiste
- Fix styling for GraphiQL on the webhook page - #3389 by @zaiste
- Copy filters automatically to GraphiQL playground - #3385 by @zaiste
- Update subscription query with Event types only and improve Event picking - #3357 by @2can
## 3.4

View file

@ -16,6 +16,7 @@ const props: WebhookDetailsPageProps = {
onSubmit: () => undefined,
saveButtonBarState: "default",
webhook,
availableEvents: [],
};
storiesOf("Apps / Webhooks / Webhook details", module)
.addDecorator(Decorator)

View file

@ -11,6 +11,7 @@ import {
createSyncEventsSelectHandler,
} from "@dashboard/custom-apps/handlers";
import { CustomAppUrls } from "@dashboard/custom-apps/urls";
import { IntrospectionNode } from "@dashboard/custom-apps/utils";
import {
WebhookDetailsFragment,
WebhookErrorFragment,
@ -49,6 +50,7 @@ export interface WebhookDetailsPageProps {
webhook?: WebhookDetailsFragment | null;
saveButtonBarState: ConfirmButtonTransitionState;
onSubmit: (data: WebhookFormData) => SubmitPromise<any[]>;
availableEvents: IntrospectionNode[];
}
const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
@ -58,6 +60,7 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
webhook,
saveButtonBarState,
onSubmit,
availableEvents,
}) => {
const intl = useIntl();
const navigate = useNavigator();
@ -100,12 +103,14 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
data,
query,
setQuery,
availableEvents,
});
const handleAsyncEventsSelect = createAsyncEventsSelectHandler({
change,
data,
query,
setQuery,
availableEvents,
});
return (

View file

@ -6,27 +6,38 @@ import { ChangeEvent } from "@dashboard/hooks/useForm";
import { capitalize } from "@dashboard/misc";
import { toggle } from "@dashboard/utils/lists";
import {
DocumentNode,
FieldNode,
InlineFragmentNode,
ObjectFieldNode,
OperationDefinitionNode,
parse,
print,
SelectionNode,
visit,
} from "graphql";
import isEmpty from "lodash/isEmpty";
import React, { Dispatch, SetStateAction } from "react";
import { WebhookFormData } from "./components/WebhookDetailsPage";
import { filterSelectedAsyncEvents } from "./utils";
import { filterSelectedAsyncEvents, IntrospectionNode } from "./utils";
interface CreateSyncEventsSelectHandler {
change: (event: ChangeEvent, cb?: () => void) => void;
data: WebhookFormData;
query: string;
setQuery: Dispatch<SetStateAction<string>>;
availableEvents: IntrospectionNode[];
}
export const createSyncEventsSelectHandler =
({ change, data, query, setQuery }: CreateSyncEventsSelectHandler) =>
({
change,
data,
query,
setQuery,
availableEvents,
}: CreateSyncEventsSelectHandler) =>
(event: ChangeEvent) => {
const { syncEvents, asyncEvents } = data;
const events = toggle(event.target.value, syncEvents, (a, b) => a === b);
@ -50,7 +61,7 @@ export const createSyncEventsSelectHandler =
},
});
handleQuery(events, query, setQuery);
handleQuery({ events, query, setQuery, availableEvents });
};
interface CreateAsyncEventsSelectHandler {
@ -58,10 +69,17 @@ interface CreateAsyncEventsSelectHandler {
data: WebhookFormData;
query: string;
setQuery: Dispatch<SetStateAction<string>>;
availableEvents: IntrospectionNode[];
}
export const createAsyncEventsSelectHandler =
({ change, data, query, setQuery }: CreateAsyncEventsSelectHandler) =>
({
change,
data,
query,
setQuery,
availableEvents,
}: CreateAsyncEventsSelectHandler) =>
(event: ChangeEvent) => {
const { syncEvents, asyncEvents } = data;
const events = toggle(event.target.value, asyncEvents, (a, b) => a === b);
@ -86,62 +104,125 @@ export const createAsyncEventsSelectHandler =
},
});
handleQuery(filteredEvents, query, setQuery);
handleQuery({ events: filteredEvents, query, setQuery, availableEvents });
};
const handleQuery = (
events: WebhookEventTypeAsyncEnum[] | WebhookEventTypeSyncEnum[],
query: string,
setQuery: React.Dispatch<React.SetStateAction<string>>,
) => {
if (events.length > 0 && query.length === 0) {
const event = events[0]
.toLowerCase()
.split("_")
.map(chunk => capitalize(chunk))
.join("");
const enumToEventName = (value: string) =>
value
.toLowerCase()
.split("_")
.map(chunk => capitalize(chunk))
.join("");
interface HandleQuery {
events: WebhookEventTypeAsyncEnum[] | WebhookEventTypeSyncEnum[];
query: string;
setQuery: React.Dispatch<React.SetStateAction<string>>;
availableEvents: IntrospectionNode[];
}
const handleQuery = ({
events,
query,
setQuery,
availableEvents,
}: HandleQuery) => {
const availableEventNames = availableEvents.map(({ name }) => name);
const eventsNames: string[] = events
.map(enumToEventName)
.filter(eventName => availableEventNames.includes(eventName));
if (eventsNames.length > 0 && query.length === 0) {
setQuery(
print(parse(`subscription { event { ... on ${event} { __typename } } }`)),
print(
parse(
`subscription { event { ... on ${eventsNames[0]} { __typename } } }`,
),
),
);
}
if (query.length > 0) {
const ast = parse(query);
try {
const ast = parse(query);
visit(ast, {
SelectionSet(node, _key, parent) {
if ((parent as ObjectFieldNode).name?.value === "event") {
const queryEvents = node.selections.map(
selection =>
(selection as InlineFragmentNode).typeCondition.name.value,
);
const newEvents = events
.map(event =>
event
.toLowerCase()
.split("_")
.map(chunk => capitalize(chunk))
.join(""),
)
.filter(event => !queryEvents.includes(event));
if (newEvents.length > 0) {
// TODO modify AST
const inserted = query.replace(/\n/g, " ").replace(
" } } ",
newEvents
.map(event => ` ... on ${event} { __typename }`)
.join("")
.concat(" } } "),
const editedAst: DocumentNode = visit(ast, {
SelectionSet(node, _key, parent) {
if ((parent as ObjectFieldNode).name?.value === "event") {
const queryEvents = node.selections.map(
selection =>
(selection as InlineFragmentNode).typeCondition.name.value,
);
setQuery(print(parse(inserted)));
}
}
return undefined;
},
});
const eventsToRemove = queryEvents.filter(
event => !eventsNames.includes(event),
);
const selections = [...node.selections].filter(
selection =>
!eventsToRemove.includes(
(selection as InlineFragmentNode).typeCondition.name.value,
),
);
eventsNames.forEach(event => {
if (!queryEvents.includes(event)) {
const eventFragment = createEventInlineFragment(event);
selections.push(eventFragment);
}
});
if (!selections.length) {
return null;
}
return {
...node,
selections,
};
}
},
});
setQuery(isEmptyQuery(editedAst) ? "" : print(editedAst));
} catch (e) {
console.error(e);
}
}
};
const createEventInlineFragment = (event: string): SelectionNode => ({
kind: "InlineFragment",
typeCondition: {
kind: "NamedType",
name: {
kind: "Name",
value: event,
},
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "__typename" } },
],
},
});
const isEmptyQuery = (ast: DocumentNode): boolean => {
let empty = false;
visit(ast, {
SelectionSet(node, _key, parent) {
if ((parent as OperationDefinitionNode).operation === "subscription") {
const event = node.selections.filter(
selection => (selection as FieldNode).name?.value === "event",
)[0] as FieldNode;
if (!event.selectionSet) {
empty = true;
}
}
},
});
return empty;
};

14
src/custom-apps/hooks.ts Normal file
View file

@ -0,0 +1,14 @@
import { useQuery } from "@apollo/client";
import { buildEventsMap, IntrospectionQuery } from "./utils";
export const useAvailableEvents = () => {
const { data: introspectionData } = useQuery(IntrospectionQuery, {
fetchPolicy: "network-only",
});
const elements = introspectionData?.__schema?.types || [];
const availableEvents = buildEventsMap(elements);
return availableEvents;
};

View file

@ -1,3 +1,4 @@
import { gql } from "@apollo/client";
import { WebhookEventTypeAsyncEnum, WebhookFragment } from "@dashboard/graphql";
export function isUnnamed(webhook: WebhookFragment | undefined): boolean {
@ -15,3 +16,31 @@ export const filterSelectedAsyncEvents = (
}
return asyncEvents;
};
export interface IntrospectionNode {
name: string;
interfaces: Array<{
name: string;
}> | null;
description: string;
}
// cannot be in `queries.ts` as codegen cannot handle `__schema`
export const IntrospectionQuery = gql`
query EventsIntrospection {
__schema {
types {
name
interfaces {
name
}
description
}
}
}
`;
const isEvent = ({ name }: { name: string }) => name === "Event";
export const buildEventsMap = (elements: IntrospectionNode[]) =>
elements.filter(({ interfaces }) => (interfaces || []).some(isEvent));

View file

@ -14,6 +14,7 @@ import { useIntl } from "react-intl";
import WebhookDetailsPage, {
WebhookFormData,
} from "../components/WebhookDetailsPage";
import { useAvailableEvents } from "../hooks";
import { CustomAppUrls } from "../urls";
export interface CustomAppWebhookCreateProps {
@ -29,6 +30,8 @@ export const CustomAppWebhookCreate: React.FC<CustomAppWebhookCreateProps> = ({
const { data } = useAppQuery({ variables: { id: appId } });
const availableEvents = useAvailableEvents();
const [webhookCreate, webhookCreateOpts] = useWebhookCreateMutation({
onCompleted: data => {
const webhook = data.webhookCreate?.webhook;
@ -80,6 +83,7 @@ export const CustomAppWebhookCreate: React.FC<CustomAppWebhookCreateProps> = ({
errors={webhookCreateOpts.data?.webhookCreate?.errors ?? []}
onSubmit={handleSubmit}
saveButtonBarState={webhookCreateOpts.status}
availableEvents={availableEvents}
/>
</>
);

View file

@ -14,6 +14,7 @@ import { extractMutationErrors, getStringOrPlaceholder } from "../../misc";
import WebhookDetailsPage, {
WebhookFormData,
} from "../components/WebhookDetailsPage";
import { useAvailableEvents } from "../hooks";
import { CustomAppUrls } from "../urls";
export interface CustomAppWebhookDetailsProps {
@ -26,6 +27,8 @@ export const CustomAppWebhookDetails: React.FC<
const notify = useNotifier();
const intl = useIntl();
const availableEvents = useAvailableEvents();
const { data: webhookDetails, loading } = useWebhookDetailsQuery({
variables: { id },
});
@ -86,6 +89,7 @@ export const CustomAppWebhookDetails: React.FC<
saveButtonBarState={webhookUpdateOpts.status}
webhook={webhook}
onSubmit={handleSubmit}
availableEvents={availableEvents}
/>
</>
);