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
|
@ -23,10 +23,11 @@ All notable, unreleased changes to this project will be documented in this file.
|
||||||
- Add custom request headers to webhook form - #3107 by @2can
|
- Add custom request headers to webhook form - #3107 by @2can
|
||||||
- Allow subscription query for sync events - #3099 by @2can
|
- Allow subscription query for sync events - #3099 by @2can
|
||||||
- Simplify codegen configuration to generate the graphql schema - #2538 by @abumalick
|
- Simplify codegen configuration to generate the graphql schema - #2538 by @abumalick
|
||||||
- Add DevMode panel (trigger: CMD+') - #3333 by @zaiste
|
- Add DevMode panel (trigger: CMD+') - #3333 by @zaiste
|
||||||
- Migrate to `es2020` for TypeScript - #3386 by @zaiste
|
- Migrate to `es2020` for TypeScript - #3386 by @zaiste
|
||||||
- Fix styling for GraphiQL on the webhook page - #3389 by @zaiste
|
- Fix styling for GraphiQL on the webhook page - #3389 by @zaiste
|
||||||
- Copy filters automatically to GraphiQL playground - #3385 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
|
## 3.4
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ const props: WebhookDetailsPageProps = {
|
||||||
onSubmit: () => undefined,
|
onSubmit: () => undefined,
|
||||||
saveButtonBarState: "default",
|
saveButtonBarState: "default",
|
||||||
webhook,
|
webhook,
|
||||||
|
availableEvents: [],
|
||||||
};
|
};
|
||||||
storiesOf("Apps / Webhooks / Webhook details", module)
|
storiesOf("Apps / Webhooks / Webhook details", module)
|
||||||
.addDecorator(Decorator)
|
.addDecorator(Decorator)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
createSyncEventsSelectHandler,
|
createSyncEventsSelectHandler,
|
||||||
} from "@dashboard/custom-apps/handlers";
|
} from "@dashboard/custom-apps/handlers";
|
||||||
import { CustomAppUrls } from "@dashboard/custom-apps/urls";
|
import { CustomAppUrls } from "@dashboard/custom-apps/urls";
|
||||||
|
import { IntrospectionNode } from "@dashboard/custom-apps/utils";
|
||||||
import {
|
import {
|
||||||
WebhookDetailsFragment,
|
WebhookDetailsFragment,
|
||||||
WebhookErrorFragment,
|
WebhookErrorFragment,
|
||||||
|
@ -49,6 +50,7 @@ export interface WebhookDetailsPageProps {
|
||||||
webhook?: WebhookDetailsFragment | null;
|
webhook?: WebhookDetailsFragment | null;
|
||||||
saveButtonBarState: ConfirmButtonTransitionState;
|
saveButtonBarState: ConfirmButtonTransitionState;
|
||||||
onSubmit: (data: WebhookFormData) => SubmitPromise<any[]>;
|
onSubmit: (data: WebhookFormData) => SubmitPromise<any[]>;
|
||||||
|
availableEvents: IntrospectionNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
||||||
|
@ -58,6 +60,7 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
||||||
webhook,
|
webhook,
|
||||||
saveButtonBarState,
|
saveButtonBarState,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
availableEvents,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
|
@ -100,12 +103,14 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
||||||
data,
|
data,
|
||||||
query,
|
query,
|
||||||
setQuery,
|
setQuery,
|
||||||
|
availableEvents,
|
||||||
});
|
});
|
||||||
const handleAsyncEventsSelect = createAsyncEventsSelectHandler({
|
const handleAsyncEventsSelect = createAsyncEventsSelectHandler({
|
||||||
change,
|
change,
|
||||||
data,
|
data,
|
||||||
query,
|
query,
|
||||||
setQuery,
|
setQuery,
|
||||||
|
availableEvents,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,27 +6,38 @@ import { ChangeEvent } from "@dashboard/hooks/useForm";
|
||||||
import { capitalize } from "@dashboard/misc";
|
import { capitalize } from "@dashboard/misc";
|
||||||
import { toggle } from "@dashboard/utils/lists";
|
import { toggle } from "@dashboard/utils/lists";
|
||||||
import {
|
import {
|
||||||
|
DocumentNode,
|
||||||
|
FieldNode,
|
||||||
InlineFragmentNode,
|
InlineFragmentNode,
|
||||||
ObjectFieldNode,
|
ObjectFieldNode,
|
||||||
|
OperationDefinitionNode,
|
||||||
parse,
|
parse,
|
||||||
print,
|
print,
|
||||||
|
SelectionNode,
|
||||||
visit,
|
visit,
|
||||||
} from "graphql";
|
} from "graphql";
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import React, { Dispatch, SetStateAction } from "react";
|
import React, { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
import { WebhookFormData } from "./components/WebhookDetailsPage";
|
import { WebhookFormData } from "./components/WebhookDetailsPage";
|
||||||
import { filterSelectedAsyncEvents } from "./utils";
|
import { filterSelectedAsyncEvents, IntrospectionNode } from "./utils";
|
||||||
|
|
||||||
interface CreateSyncEventsSelectHandler {
|
interface CreateSyncEventsSelectHandler {
|
||||||
change: (event: ChangeEvent, cb?: () => void) => void;
|
change: (event: ChangeEvent, cb?: () => void) => void;
|
||||||
data: WebhookFormData;
|
data: WebhookFormData;
|
||||||
query: string;
|
query: string;
|
||||||
setQuery: Dispatch<SetStateAction<string>>;
|
setQuery: Dispatch<SetStateAction<string>>;
|
||||||
|
availableEvents: IntrospectionNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSyncEventsSelectHandler =
|
export const createSyncEventsSelectHandler =
|
||||||
({ change, data, query, setQuery }: CreateSyncEventsSelectHandler) =>
|
({
|
||||||
|
change,
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
availableEvents,
|
||||||
|
}: CreateSyncEventsSelectHandler) =>
|
||||||
(event: ChangeEvent) => {
|
(event: ChangeEvent) => {
|
||||||
const { syncEvents, asyncEvents } = data;
|
const { syncEvents, asyncEvents } = data;
|
||||||
const events = toggle(event.target.value, syncEvents, (a, b) => a === b);
|
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 {
|
interface CreateAsyncEventsSelectHandler {
|
||||||
|
@ -58,10 +69,17 @@ interface CreateAsyncEventsSelectHandler {
|
||||||
data: WebhookFormData;
|
data: WebhookFormData;
|
||||||
query: string;
|
query: string;
|
||||||
setQuery: Dispatch<SetStateAction<string>>;
|
setQuery: Dispatch<SetStateAction<string>>;
|
||||||
|
availableEvents: IntrospectionNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createAsyncEventsSelectHandler =
|
export const createAsyncEventsSelectHandler =
|
||||||
({ change, data, query, setQuery }: CreateAsyncEventsSelectHandler) =>
|
({
|
||||||
|
change,
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
availableEvents,
|
||||||
|
}: CreateAsyncEventsSelectHandler) =>
|
||||||
(event: ChangeEvent) => {
|
(event: ChangeEvent) => {
|
||||||
const { syncEvents, asyncEvents } = data;
|
const { syncEvents, asyncEvents } = data;
|
||||||
const events = toggle(event.target.value, asyncEvents, (a, b) => a === b);
|
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 = (
|
const enumToEventName = (value: string) =>
|
||||||
events: WebhookEventTypeAsyncEnum[] | WebhookEventTypeSyncEnum[],
|
value
|
||||||
query: string,
|
.toLowerCase()
|
||||||
setQuery: React.Dispatch<React.SetStateAction<string>>,
|
.split("_")
|
||||||
) => {
|
.map(chunk => capitalize(chunk))
|
||||||
if (events.length > 0 && query.length === 0) {
|
.join("");
|
||||||
const event = events[0]
|
|
||||||
.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(
|
setQuery(
|
||||||
print(parse(`subscription { event { ... on ${event} { __typename } } }`)),
|
print(
|
||||||
|
parse(
|
||||||
|
`subscription { event { ... on ${eventsNames[0]} { __typename } } }`,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.length > 0) {
|
if (query.length > 0) {
|
||||||
const ast = parse(query);
|
try {
|
||||||
|
const ast = parse(query);
|
||||||
|
|
||||||
visit(ast, {
|
const editedAst: DocumentNode = visit(ast, {
|
||||||
SelectionSet(node, _key, parent) {
|
SelectionSet(node, _key, parent) {
|
||||||
if ((parent as ObjectFieldNode).name?.value === "event") {
|
if ((parent as ObjectFieldNode).name?.value === "event") {
|
||||||
const queryEvents = node.selections.map(
|
const queryEvents = node.selections.map(
|
||||||
selection =>
|
selection =>
|
||||||
(selection as InlineFragmentNode).typeCondition.name.value,
|
(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(" } } "),
|
|
||||||
);
|
);
|
||||||
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";
|
import { WebhookEventTypeAsyncEnum, WebhookFragment } from "@dashboard/graphql";
|
||||||
|
|
||||||
export function isUnnamed(webhook: WebhookFragment | undefined): boolean {
|
export function isUnnamed(webhook: WebhookFragment | undefined): boolean {
|
||||||
|
@ -15,3 +16,31 @@ export const filterSelectedAsyncEvents = (
|
||||||
}
|
}
|
||||||
return asyncEvents;
|
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, {
|
import WebhookDetailsPage, {
|
||||||
WebhookFormData,
|
WebhookFormData,
|
||||||
} from "../components/WebhookDetailsPage";
|
} from "../components/WebhookDetailsPage";
|
||||||
|
import { useAvailableEvents } from "../hooks";
|
||||||
import { CustomAppUrls } from "../urls";
|
import { CustomAppUrls } from "../urls";
|
||||||
|
|
||||||
export interface CustomAppWebhookCreateProps {
|
export interface CustomAppWebhookCreateProps {
|
||||||
|
@ -29,6 +30,8 @@ export const CustomAppWebhookCreate: React.FC<CustomAppWebhookCreateProps> = ({
|
||||||
|
|
||||||
const { data } = useAppQuery({ variables: { id: appId } });
|
const { data } = useAppQuery({ variables: { id: appId } });
|
||||||
|
|
||||||
|
const availableEvents = useAvailableEvents();
|
||||||
|
|
||||||
const [webhookCreate, webhookCreateOpts] = useWebhookCreateMutation({
|
const [webhookCreate, webhookCreateOpts] = useWebhookCreateMutation({
|
||||||
onCompleted: data => {
|
onCompleted: data => {
|
||||||
const webhook = data.webhookCreate?.webhook;
|
const webhook = data.webhookCreate?.webhook;
|
||||||
|
@ -80,6 +83,7 @@ export const CustomAppWebhookCreate: React.FC<CustomAppWebhookCreateProps> = ({
|
||||||
errors={webhookCreateOpts.data?.webhookCreate?.errors ?? []}
|
errors={webhookCreateOpts.data?.webhookCreate?.errors ?? []}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
saveButtonBarState={webhookCreateOpts.status}
|
saveButtonBarState={webhookCreateOpts.status}
|
||||||
|
availableEvents={availableEvents}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { extractMutationErrors, getStringOrPlaceholder } from "../../misc";
|
||||||
import WebhookDetailsPage, {
|
import WebhookDetailsPage, {
|
||||||
WebhookFormData,
|
WebhookFormData,
|
||||||
} from "../components/WebhookDetailsPage";
|
} from "../components/WebhookDetailsPage";
|
||||||
|
import { useAvailableEvents } from "../hooks";
|
||||||
import { CustomAppUrls } from "../urls";
|
import { CustomAppUrls } from "../urls";
|
||||||
|
|
||||||
export interface CustomAppWebhookDetailsProps {
|
export interface CustomAppWebhookDetailsProps {
|
||||||
|
@ -26,6 +27,8 @@ export const CustomAppWebhookDetails: React.FC<
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const availableEvents = useAvailableEvents();
|
||||||
|
|
||||||
const { data: webhookDetails, loading } = useWebhookDetailsQuery({
|
const { data: webhookDetails, loading } = useWebhookDetailsQuery({
|
||||||
variables: { id },
|
variables: { id },
|
||||||
});
|
});
|
||||||
|
@ -86,6 +89,7 @@ export const CustomAppWebhookDetails: React.FC<
|
||||||
saveButtonBarState={webhookUpdateOpts.status}
|
saveButtonBarState={webhookUpdateOpts.status}
|
||||||
webhook={webhook}
|
webhook={webhook}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
availableEvents={availableEvents}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue