console: schema change alerts UI

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9443
Co-authored-by: Rishichandra Wawhal <27274869+wawhal@users.noreply.github.com>
GitOrigin-RevId: a4eb6bd7922792743fe21bb6a806a5a3e3379ede
This commit is contained in:
nevermore 2023-06-15 00:58:41 +05:30 committed by hasura-bot
parent 44dd00fc37
commit 9e5795ec0f
9 changed files with 383 additions and 6 deletions

View File

@ -0,0 +1,109 @@
import React from 'react';
import { Dialog } from '../../../new-components/Dialog';
import { Checkbox } from '../../../new-components/Form';
import { FaClock, FaBan, FaBell } from 'react-icons/fa';
import { useGetAlertConfig } from '../hooks/useGetAlertConfig';
import { CapitalizeFirstLetter } from '../utils';
import globals from '../../../Globals';
import { AlertHeader } from './AlertsHeader';
import { CustomDialogFooter } from './CustomDialogFooter';
import { ConfigKey } from '../types';
type DialogProps = {
onClose: () => void;
};
const configKeys: ConfigKey[] = ['breaking', 'dangerous', 'safe'];
const defaultAlertConfig: Record<string, boolean> = {
safe: true,
breaking: true,
dangerous: true,
};
export const AlertsDialog: React.FC<DialogProps> = ({ onClose }) => {
const projectID = globals.hasuraCloudProjectId || '';
const fetchAlertConfigResponse = useGetAlertConfig(projectID);
const [config, setConfig] =
React.useState<Record<ConfigKey, boolean>>(defaultAlertConfig);
const { kind } = fetchAlertConfigResponse;
const alertConfig =
kind === 'success' &&
fetchAlertConfigResponse.response.schema_registry_alerts.length
? fetchAlertConfigResponse.response.schema_registry_alerts[0].config
: defaultAlertConfig;
// initialise checkboxes
React.useEffect(() => {
if (kind === 'success') {
setConfig(alertConfig);
}
}, [alertConfig]);
return (
<Dialog
hasBackdrop
onClose={kind === 'error' ? onClose : undefined}
footer={
kind === 'success' ? (
<CustomDialogFooter onClose={onClose} alertConfig={config} />
) : undefined
}
>
{kind === 'loading' ? (
<div className="flex items-top p-md">
<AlertHeader
icon={<FaClock className="w-9 h-9 mt-sm mr-md fill-current" />}
title="Loading..."
/>
</div>
) : kind === 'error' ? (
<div className="flex items-top p-md">
<AlertHeader
icon={<FaBan className="w-9 h-9 mt-sm mr-md fill-red-500" />}
title="Error"
description={fetchAlertConfigResponse.message}
/>
</div>
) : (
<>
<div className="flex items-top p-md">
<div className="text-yellow-500">
<FaBell className="w-9 h-9 mt-sm mr-md fill-current" />
</div>
<div>
<p className="font-semibold">Email Alerts</p>
<div className="overflow-y-auto max-h-[calc(100vh-14rem)]">
<p className="m-0">
Select the change categories for which an email should be
sent!
</p>
</div>
</div>
</div>
<div className="flex flex-col ml-8">
{configKeys.map(c => {
return (
<div
className="flex items-center mb-xs cursor-pointer w-auto"
role="checkbox"
onClick={() => {
setConfig(prevConfig => ({
...prevConfig,
[c]: !prevConfig[c],
}));
}}
>
<Checkbox checked={config[c]} />
<p>{CapitalizeFirstLetter(c)}&nbsp;changes</p>
</div>
);
})}
</div>
</>
)}
</Dialog>
);
};

View File

@ -0,0 +1,27 @@
import React from 'react';
type AlertHeaderProps = {
icon: React.ReactNode | React.ReactElement;
title: string;
description?: string;
};
export const AlertHeader: React.FC<AlertHeaderProps> = ({
icon,
title,
description,
}) => {
return (
<>
<div className="text-yellow-500">{icon}</div>
<div>
<p className="font-semibold">{title}</p>
{description && (
<div className="overflow-y-auto max-h-[calc(100vh-14rem)]">
<p className="m-0">{description}</p>
</div>
)}
</div>
</>
);
};

View File

@ -1,19 +1,36 @@
import * as React from 'react';
import { useState } from 'react';
import { SchemasList } from './SchemasList';
import { FeatureRequest } from './FeatureRequest';
import globals from '../../../Globals';
import { SCHEMA_REGISTRY_FEATURE_NAME } from '../constants';
import { FaBell } from 'react-icons/fa';
import { IconTooltip } from '../../../new-components/Tooltip';
import { AlertsDialog } from './AlertsDialog';
import { Badge } from '../../../new-components/Badge';
import { SCHEMA_REGISTRY_REF_URL } from '../constants';
const Header: React.VFC = () => {
const [isAlertModalOpen, setIsAlertModalOpen] = useState(false);
return (
<div className="flex flex-col w-full">
<div className="flex w-full mb-sm">
<h1 className="text-xl font-semibold">GraphQL Schema Registry</h1>
<Badge className="mx-2" color="blue">
BETA
</Badge>
<div className="flex w-3/5 mb-sm justify-between">
<div className="flex items-center">
<h1 className="text-xl font-semibold">GraphQL Schema Registry</h1>
<Badge className="mx-2" color="blue">
BETA
</Badge>
</div>
<div
className="flex text-lg mt-2 mr-2 cursor-pointer"
role="button"
onClick={() => setIsAlertModalOpen(true)}
>
<IconTooltip
message="Alerts on GraphQL schema changes"
icon={<FaBell />}
/>
</div>
</div>
<a
className="text-muted w-auto"
@ -23,6 +40,9 @@ const Header: React.VFC = () => {
>
What is Schema Registry?
</a>
{isAlertModalOpen && (
<AlertsDialog onClose={() => setIsAlertModalOpen(false)} />
)}
</div>
);
};

View File

@ -0,0 +1,48 @@
import React from 'react';
import globals from '../../../Globals';
import { Button } from '../../../new-components/Button';
import { useSetEmailAlertConfig } from '../hooks/useSetAlertConfig';
import { ConfigKey } from '../types';
type CustomDialogFooterProps = {
onClose: () => void;
alertConfig: Record<ConfigKey, boolean>;
};
export const CustomDialogFooter: React.FC<CustomDialogFooterProps> = ({
onClose,
alertConfig,
}) => {
const projectID = globals.hasuraCloudProjectId || '';
const { setEmailAlertMutation } = useSetEmailAlertConfig(onClose);
const onSet = React.useCallback(() => {
setEmailAlertMutation.mutate({ projectId: projectID, config: alertConfig });
}, [alertConfig]);
return (
<div className="flex justify-between border-t border-gray-300 bg-white p-sm">
<div>
<p className="text-muted">
Email alerts will be sent to the owner of this project.
</p>
</div>
<div className="flex">
<Button onClick={onClose}>Cancel</Button>
<div className="ml-2">
<Button
mode="primary"
onClick={e => {
e.preventDefault();
onSet();
}}
isLoading={setEmailAlertMutation.isLoading}
disabled={setEmailAlertMutation.isLoading}
>
Set
</Button>
</div>
</div>
</div>
);
};

View File

@ -2,6 +2,7 @@ export const FETCH_REGISTRY_SCHEMAS_QUERY_NAME =
'FETCH_REGISTRY_SCHEMAS_QUERY_NAME';
export const FETCH_REGISTRY_SCHEMA_QUERY_NAME =
'FETCH_REGISTRY_SCHEMA_QUERY_NAME';
export const FETCH_ALERT_CONFIG_QUERY_NAME = 'FETCH_ALERT_CONFIG_QUERY_NAME';
// 5 minutes as default stale time
export const SCHEMA_REGISTRY_REFRESH_TIME = 5 * 60 * 1000;

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import { useQuery } from 'react-query';
import { FETCH_ALERT_CONFIG } from '../queries';
import { schemaRegsitryControlPlaneClient } from '../utils';
import { GetAlertConfigResponseWithError } from '../types';
import { FETCH_ALERT_CONFIG_QUERY_NAME } from '../constants';
type FetchAlertResponse =
| {
kind: 'loading';
}
| {
kind: 'error';
message: string;
}
| {
kind: 'success';
response: NonNullable<GetAlertConfigResponseWithError['data']>;
};
export const useGetAlertConfig = (projectId: string): FetchAlertResponse => {
const fetchAlertConfigQueryFn = (projectId: string) => {
return schemaRegsitryControlPlaneClient.query<
GetAlertConfigResponseWithError,
{ projectId: string }
>(FETCH_ALERT_CONFIG, {
projectId: projectId,
});
};
const { data, error, isLoading } = useQuery({
queryKey: FETCH_ALERT_CONFIG_QUERY_NAME,
queryFn: () => fetchAlertConfigQueryFn(projectId),
refetchOnMount: true,
});
if (isLoading) {
return {
kind: 'loading',
};
}
if (error || !data || !!data.errors || !data.data) {
return {
kind: 'error',
message: 'error',
};
}
return {
kind: 'success',
response: data.data,
};
};

View File

@ -0,0 +1,58 @@
import * as React from 'react';
import { useMutation } from 'react-query';
import { SET_ALERT_CONFIG } from '../queries';
import { schemaRegsitryControlPlaneClient } from '../utils';
import { SetAlertConfigResponseWithError } from '../types';
import { FETCH_ALERT_CONFIG_QUERY_NAME } from '../constants';
import { reactQueryClient } from '../../../lib/reactQuery';
import { hasuraToast } from '../../../new-components/Toasts';
type setEmailAlertMutationFnArgs = {
projectId: string;
config: Record<string, boolean>;
};
export const useSetEmailAlertConfig = (onSuccess: VoidFunction) => {
const setSchemaRegistryEmailAlertMutationFn = (variables: {
projectId: string;
config: Record<string, boolean>;
}) => {
return schemaRegsitryControlPlaneClient.query<
SetAlertConfigResponseWithError,
{ projectId: string; config: Record<string, boolean> }
>(SET_ALERT_CONFIG, variables);
};
const setEmailAlertMutation = useMutation(
(args: setEmailAlertMutationFnArgs) =>
setSchemaRegistryEmailAlertMutationFn(args),
{
onSuccess: () => {
hasuraToast({
type: 'success',
title: 'Success!',
message: 'Email configuration set successfully!',
});
onSuccess();
},
onError: () => {
hasuraToast({
type: 'error',
title: 'Error!',
message:
'Something went wrong while setting alerts for Schema Registry',
});
},
onSettled: response => {
if (response?.data) {
reactQueryClient.refetchQueries(FETCH_ALERT_CONFIG_QUERY_NAME);
}
},
}
);
return {
setEmailAlertMutation,
};
};

View File

@ -58,3 +58,27 @@ query fetchRegistrySchema ($schemaId: uuid!) {
}
}
`);
export const FETCH_ALERT_CONFIG = gql(`
query QueryAlerts($projectId: uuid!) {
schema_registry_alerts(where: {project_id: {_eq: $projectId}}) {
config
alert_type
id
meta
project_id
slack_webhook
}
}
`);
export const SET_ALERT_CONFIG = gql(`
mutation UpsertEmailAlertConfig($projectId: uuid, $config: jsonb) {
insert_schema_registry_alerts_one(object: {alert_type: email, config: $config, project_id: $projectId}, on_conflict: {constraint: schema_registry_alerts_project_id_alert_type_key, update_columns: config}) {
id
project_id
alert_type
config
}
}
`);

View File

@ -86,3 +86,39 @@ export type GetRegistrySchemaResponseWithError = {
export type GetRegistrySchemaQueryResponse = {
schema_registry_dumps: SiblingSchema[];
};
export type GetAlertConfigResponseWithError = {
data?: GetAlertConfigQueryResponse;
errors?: GraphQLError[];
};
export type GetAlertConfigQueryResponse = {
schema_registry_alerts: SchemaRegistryAlert[];
};
export type SchemaRegistryAlert = {
id: string;
project_id: string;
alert_type: string;
config: Record<string, boolean>;
slack_webhook: string;
meta: Record<string, any>;
};
export type SetAlertConfigResponseWithError = {
data?: SetAlertConfigQueryResponse;
errors?: GraphQLError[];
};
export type SetAlertConfigQueryResponse = {
schema_registry_alerts: SchemaRegistryAlert;
};
export type SetSchemaRegistryAlert = {
id: string;
project_id: string;
alert_type: string;
config: Record<string, boolean>;
};
export type ConfigKey = 'safe' | 'dangerous' | 'breaking';