Update subscription query with Event types only and improve Event picking (#3357)
This commit is contained in:
parent
b36b4859f1
commit
5dabfc315f
8 changed files with 191 additions and 52 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ const props: WebhookDetailsPageProps = {
|
|||
onSubmit: () => undefined,
|
||||
saveButtonBarState: "default",
|
||||
webhook,
|
||||
availableEvents: [],
|
||||
};
|
||||
storiesOf("Apps / Webhooks / Webhook details", module)
|
||||
.addDecorator(Decorator)
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
14
src/custom-apps/hooks.ts
Normal 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;
|
||||
};
|
|
@ -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));
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue