console: Schema registry notification dot( cherrypick v2.34)

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/10431
Co-authored-by: ojas <31686586+OjasWadhwani@users.noreply.github.com>
GitOrigin-RevId: 8ad8590a44924dfa5aeec2a29e02dafd1d11e01e
This commit is contained in:
Aaysha 2023-11-18 01:17:08 +05:30 committed by hasura-bot
parent d892851183
commit 3c3e359457
7 changed files with 177 additions and 21 deletions

View File

@ -4,8 +4,19 @@ import { isProConsole } from '../../../utils/proConsole';
import { useEELiteAccess } from '../../../features/EETrial'; import { useEELiteAccess } from '../../../features/EETrial';
import globals from '../../../Globals'; import globals from '../../../Globals';
import { IconTooltip } from '../../../new-components/Tooltip'; import { IconTooltip } from '../../../new-components/Tooltip';
import { FaRegMap } from 'react-icons/fa'; import { FaExclamationCircle, FaRegMap } from 'react-icons/fa';
import { sendTelemetryEvent } from '../../../telemetry'; import { sendTelemetryEvent } from '../../../telemetry';
import { useGetSchemaRegistryNotificationColor } from '../../../features/SchemaRegistry/hooks/useGetSchemaRegistryNotificationColor';
import { getLSItem, LS_KEYS, setLSItem } from '../../../utils';
import {
BreakingChangesColor,
BreakingChangesTooltipMessage,
DangerousChangesColor,
DangerousChangesTooltipMessage,
DefaultToopTipMessage,
SafeChangesColor,
SafeChangesTooltipMessage,
} from '../../../features/SchemaRegistry/constants';
type TopNavProps = { type TopNavProps = {
location: RouteComponentProps<unknown, unknown>['location']; location: RouteComponentProps<unknown, unknown>['location'];
@ -60,7 +71,54 @@ const TopNav: React.FC<TopNavProps> = ({ location }) => {
} }
return location.pathname.includes(link); return location.pathname.includes(link);
}; };
const projectID = globals.hasuraCloudProjectId || '';
const fetchSchemaRegistryNotificationData =
useGetSchemaRegistryNotificationColor(projectID);
let color = '';
let tooltipMessage = DefaultToopTipMessage;
let change_recorded_at = '';
let showNotifications = false;
if (fetchSchemaRegistryNotificationData.kind === 'success') {
const data =
fetchSchemaRegistryNotificationData?.response
?.schema_registry_dumps_v2[0] || [];
if (
data &&
data.diff_with_previous_schema &&
data.diff_with_previous_schema[0] &&
data.diff_with_previous_schema[0].schema_diff_data &&
data.change_recorded_at
) {
const changes = data.diff_with_previous_schema[0].schema_diff_data;
// Check if there's a change with a criticality level of "BREAKING"
const hasBreakingChange = changes.some(
change => change.criticality && change.criticality.level === 'BREAKING'
);
const hasDangerousChange = changes.some(
change => change.criticality && change.criticality.level === 'DANGEROUS'
);
const last_viewed_change = getLSItem(LS_KEYS.lastViewedSchemaChange);
if (
(!last_viewed_change || last_viewed_change < data.change_recorded_at) &&
changes
) {
if (hasBreakingChange) {
color = BreakingChangesColor;
tooltipMessage = BreakingChangesTooltipMessage;
} else if (hasDangerousChange) {
//gold color instead of yellow to be more visible
color = DangerousChangesColor;
tooltipMessage = DangerousChangesTooltipMessage;
} else {
color = SafeChangesColor;
tooltipMessage = SafeChangesTooltipMessage;
}
change_recorded_at = data.change_recorded_at;
showNotifications = true;
}
}
}
return ( return (
<div className="flex justify-between items-center border-b border-gray-300 bg-white px-sm"> <div className="flex justify-between items-center border-b border-gray-300 bg-white px-sm">
<div className="flex px-1 w-full"> <div className="flex px-1 w-full">
@ -78,6 +136,9 @@ const TopNav: React.FC<TopNavProps> = ({ location }) => {
}`} }`}
key={section.key} key={section.key}
onClick={() => { onClick={() => {
if (showNotifications) {
setLSItem(LS_KEYS.lastViewedSchemaChange, change_recorded_at);
}
// Send Telemetry data for Schema Registry tab // Send Telemetry data for Schema Registry tab
if (section.key === 'schema-registry') { if (section.key === 'schema-registry') {
sendTelemetryEvent({ sendTelemetryEvent({
@ -97,8 +158,14 @@ const TopNav: React.FC<TopNavProps> = ({ location }) => {
{section.title} {section.title}
{section.key === 'schema-registry' && ( {section.key === 'schema-registry' && (
<IconTooltip <IconTooltip
icon={<FaRegMap />} icon={
message="Detect breaking and dangerous changes, view schema change history. Keep your GraphQL services safe and reliable! 🚀" color ? (
<FaExclamationCircle style={{ color }} />
) : (
<FaRegMap />
)
}
message={tooltipMessage}
/> />
)} )}
</Link> </Link>

View File

@ -380,26 +380,28 @@ const SchemaCard: React.VFC<{
</div> </div>
<div> <div>
{RolesList.map((roleBasedChange, index) => ( {RolesList.map((roleBasedChange, index) => (
<div <Analytics name="schema-registry-schema-change-card">
className={`flex w-full px-2 py-1 ${ <div
isCurrentCardOpen && roleBasedChange.id === selectedRoleID className={`flex w-full px-2 py-1 ${
? 'bg-gray-200' isCurrentCardOpen && roleBasedChange.id === selectedRoleID
: '' ? 'bg-gray-200'
} rounded hover:bg-gray-200`} : ''
onClick={() => { } rounded hover:bg-gray-200`}
handleRoleClick(roleBasedChange); onClick={() => {
}} handleRoleClick(roleBasedChange);
key={index} }}
> key={index}
<div className="flex items-center justify-between w-full rounded"> >
<div className="text-base rounded cursor-pointer"> <div className="flex items-center justify-between w-full rounded">
<p className="text-sm text-teal-800 font-bold bg-gray-200 px-1 rounded"> <div className="text-base rounded cursor-pointer">
{CapitalizeFirstLetter(roleBasedChange.role)} <p className="text-sm text-teal-800 font-bold bg-gray-200 px-1 rounded">
</p> {CapitalizeFirstLetter(roleBasedChange.role)}
</p>
</div>
<FaChevronRight />
</div> </div>
<FaChevronRight />
</div> </div>
</div> </Analytics>
))} ))}
{!defaultShowAllRoles && ( {!defaultShowAllRoles && (
<Analytics name="schema-registry-see-more-roles-btn"> <Analytics name="schema-registry-see-more-roles-btn">

View File

@ -17,6 +17,8 @@ export const FETCH_SCHEMA_REGISTRY_DUMPS_V2_QUERY_NAME =
export const FETCH_SCHEMA_REGISTRY_DUMPS_V1_QUERY_NAME = export const FETCH_SCHEMA_REGISTRY_DUMPS_V1_QUERY_NAME =
'FETCH_SCHEMA_REGISTRY_DUMPS_V1_QUERY_NAME'; 'FETCH_SCHEMA_REGISTRY_DUMPS_V1_QUERY_NAME';
export const FETCH_SCHEMA_REGISTRY_NOTIFICATION_QUERY_NAME =
'FETCH_SCHEMA_REGISTRY_NOTIFICATION_QUERY_NAME';
// 5 minutes as default stale time // 5 minutes as default stale time
export const SCHEMA_REGISTRY_REFRESH_TIME = 5 * 60 * 1000; export const SCHEMA_REGISTRY_REFRESH_TIME = 5 * 60 * 1000;
@ -29,3 +31,14 @@ export const SCHEMA_LIST_FETCH_BATCH_SIZE = 10;
export const EMPTY_UUID_STRING = '00000000-0000-0000-0000-000000000000'; export const EMPTY_UUID_STRING = '00000000-0000-0000-0000-000000000000';
export const DEFAULT_TAG_COLOR = '#000000'; export const DEFAULT_TAG_COLOR = '#000000';
export const BreakingChangesTooltipMessage = 'Breaking changes detected!';
export const DangerousChangesTooltipMessage = 'Dangerous changes detected!';
export const SafeChangesTooltipMessage = 'Checkout the Latest Schema Change';
export const DefaultToopTipMessage =
'Detect breaking and dangerous changes, view schema change history. Keep your GraphQL services safe and reliable! 🚀';
export const BreakingChangesColor = 'red';
export const DangerousChangesColor = '#FFD700';
export const SafeChangesColor = 'green';

View File

@ -0,0 +1,52 @@
import { useQuery } from 'react-query';
import { FETCH_SCHEMA_REGISTRY_NOTIFICATION_QUERY_NAME } from '../constants';
import { FETCH_SCHEMA_REGISTRY_NOTIFICATION_QUERY } from '../queries';
import { GetSchemaRegistryNotificationResponseWithError } from '../types';
import { schemaRegsitryControlPlaneClient } from '../utils';
type FetchSchemaRegistryNotificationResponse =
| {
kind: 'loading';
}
| {
kind: 'error';
}
| {
kind: 'success';
response: NonNullable<
GetSchemaRegistryNotificationResponseWithError['data']
>;
};
export const useGetSchemaRegistryNotificationColor = (
projectId: string
): FetchSchemaRegistryNotificationResponse => {
const fetchSchemaRegistyNotificationFn = (projectId: string) => {
return schemaRegsitryControlPlaneClient.query<
GetSchemaRegistryNotificationResponseWithError,
{ projectId: string }
>(FETCH_SCHEMA_REGISTRY_NOTIFICATION_QUERY, {
projectId: projectId,
});
};
const { data, error, isLoading } = useQuery({
queryKey: FETCH_SCHEMA_REGISTRY_NOTIFICATION_QUERY_NAME,
queryFn: () => fetchSchemaRegistyNotificationFn(projectId),
refetchOnMount: 'always',
refetchOnWindowFocus: true,
});
if (isLoading) {
return {
kind: 'loading',
};
}
if (error || !data || !!data?.errors || !data?.data) {
return {
kind: 'error',
};
}
return {
kind: 'success',
response: data.data,
};
};

View File

@ -134,6 +134,16 @@ query fetchSchemaRegistryDumpsV2($projectId: uuid!, $limit: Int!, $offset: Int!)
} }
} }
`); `);
export const FETCH_SCHEMA_REGISTRY_NOTIFICATION_QUERY = gql(`
query fetchSchemaRegistryDumpsV2($projectId: uuid!) {
schema_registry_dumps_v2(where: {_and: [{project_id: {_eq: $projectId}, hasura_schema_role: {_eq: "admin"}}]}, order_by: {change_recorded_at: desc}, limit: 1) {
change_recorded_at
diff_with_previous_schema {
schema_diff_data
}
}
}
`);
export const FETCH_SCHEMA_REGISTRY_DUMPS_V1_QUERY = gql(` export const FETCH_SCHEMA_REGISTRY_DUMPS_V1_QUERY = gql(`
query fetchSchemaRegistryDumpsV1($projectId: uuid!, $limit: Int!, $offset: Int!, $changeTimestamp: timestamptz!) { query fetchSchemaRegistryDumpsV1($projectId: uuid!, $limit: Int!, $offset: Int!, $changeTimestamp: timestamptz!) {

View File

@ -90,6 +90,11 @@ export type GetSchemaRegstiryDumpsV2AggregateResponseWithError = {
errors?: GraphQLError[]; errors?: GraphQLError[];
}; };
export type GetSchemaRegistryNotificationResponseWithError = {
data?: GetSchemaRegistryNotificationResponse;
errors?: GraphQLError[];
};
export type GetSchemaRegstiryDumpsV2AggregateResponse = { export type GetSchemaRegstiryDumpsV2AggregateResponse = {
schema_registry_dumps_v2_aggregate: SchemaRegistryDumpsAggregate; schema_registry_dumps_v2_aggregate: SchemaRegistryDumpsAggregate;
schema_registry_dumps_v2: SchemaRegistryChangeRecordedAt[]; schema_registry_dumps_v2: SchemaRegistryChangeRecordedAt[];
@ -101,6 +106,12 @@ export type GetSchemaRegstiryDumpsV1AggregateResponseWithError = {
export type GetSchemaRegstiryDumpsV1AggregateResponse = { export type GetSchemaRegstiryDumpsV1AggregateResponse = {
schema_registry_dumps_aggregate: SchemaRegistryDumpsAggregate; schema_registry_dumps_aggregate: SchemaRegistryDumpsAggregate;
}; };
export type GetSchemaRegistryNotificationResponse = {
schema_registry_dumps_v2: SchemaRegsitryNotificationData[];
};
export type SchemaRegsitryNotificationData = SchemaRegistryChangeRecordedAt & {
diff_with_previous_schema: SchemaDiffData[];
};
export type SchemaRegistryChangeRecordedAt = { export type SchemaRegistryChangeRecordedAt = {
change_recorded_at: string; change_recorded_at: string;
}; };

View File

@ -126,6 +126,7 @@ export const LS_KEYS = {
notificationsLastSeen: 'notifications:lastSeen', notificationsLastSeen: 'notifications:lastSeen',
authState: 'AUTH_STATE', authState: 'AUTH_STATE',
skipOnboarding: 'SKIP_CLOUD_ONBOARDING', skipOnboarding: 'SKIP_CLOUD_ONBOARDING',
lastViewedSchemaChange: 'LAST_VIEWED_SCHEMA_CHANGE',
}; };
export const clearGraphiqlLS = () => { export const clearGraphiqlLS = () => {