mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
frontend: ui for schema registry v1
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9189 Co-authored-by: nevermore <31686586+OjasWadhwani@users.noreply.github.com> GitOrigin-RevId: 8a9091442967cb1603c04e5951e2dc2adf7d918b
This commit is contained in:
parent
34e172d576
commit
44aa0f00e8
@ -3,6 +3,9 @@ schema:
|
||||
- 'http://data.lux-dev.hasura.me/v1/graphql':
|
||||
headers:
|
||||
'x-hasura-admin-secret': 'randomsecret'
|
||||
- 'http://schema-registry.lux-dev.hasura.me/v1/graphql':
|
||||
headers:
|
||||
'x-hasura-admin-secret': 'randomsecret'
|
||||
documents: 'libs/console/legacy-ce/src/lib/features/ControlPlane/queries.ts'
|
||||
generates:
|
||||
libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts:
|
||||
|
@ -98,6 +98,10 @@ export {
|
||||
export { default as Spinner } from './lib/components/Common/Spinner/Spinner';
|
||||
export { CommonScss };
|
||||
export * from './lib/components/Services/Settings';
|
||||
export {
|
||||
SchemaRegistryContainer,
|
||||
SchemaDetailsView,
|
||||
} from './lib/features/SchemaRegistry';
|
||||
export {
|
||||
loadInconsistentObjects,
|
||||
exportMetadata,
|
||||
|
@ -33,6 +33,7 @@ export const getEndpoints = (globals: typeof consoleGlobals) => {
|
||||
}/v1/graphql`,
|
||||
prometheusUrl: `${baseUrl}/v1/metrics`,
|
||||
registerEETrial: `https://licensing.pro.hasura.io/v1/graphql`,
|
||||
schemaRegistry: 'http://schema-registry.lux-dev.hasura.me/v1/graphql',
|
||||
// registerEETrial: `http://licensing.lux-dev.hasura.me/v1/graphql`,
|
||||
};
|
||||
|
||||
|
@ -161,6 +161,7 @@ export const eventsPrefix = 'events';
|
||||
export const scheduledEventsPrefix = 'cron';
|
||||
export const adhocEventsPrefix = 'one-off-scheduled-events';
|
||||
export const dataEventsPrefix = 'data';
|
||||
export const schemaRegsitryPrefix = 'sh';
|
||||
|
||||
export const getSTRoute = (type: string | undefined, relativeRoute: string) => {
|
||||
if (type === 'relative') {
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
|
||||
import { useEELiteAccess } from '../../../features/EETrial';
|
||||
import { getQueryResponseCachingRoute } from '../../../utils/routeUtils';
|
||||
import { isCloudConsole } from '../../../utils/cloudConsole';
|
||||
|
||||
export interface Metadata {
|
||||
inconsistentObjects: Record<string, unknown>[];
|
||||
@ -31,7 +32,8 @@ type SectionDataKey =
|
||||
| 'security'
|
||||
| 'monitoring'
|
||||
| 'performance'
|
||||
| 'about';
|
||||
| 'about'
|
||||
| 'graphql';
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ location, metadata }) => {
|
||||
const eeLiteAccess = useEELiteAccess(globals);
|
||||
@ -45,6 +47,21 @@ const Sidebar: React.FC<SidebarProps> = ({ location, metadata }) => {
|
||||
items: [],
|
||||
};
|
||||
|
||||
if (isCloudConsole(globals)) {
|
||||
sectionsData.graphql = {
|
||||
key: 'graphql',
|
||||
label: 'GraphQL',
|
||||
items: [
|
||||
{
|
||||
key: 'schema-registry',
|
||||
label: 'Schema Registry',
|
||||
route: '/settings/schema-registry',
|
||||
dataTestVal: 'metadata-schema-registry-link',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
sectionsData.metadata.items.push({
|
||||
key: 'actions',
|
||||
label: 'Metadata Actions',
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -248,3 +248,18 @@ mutation addSurveyAnswerV2 ($responses: [SurveyResponseV2]!, $surveyName: String
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
/**
|
||||
* GraphQL mutation to accept schema registry feature requests
|
||||
* */
|
||||
|
||||
export const ADD_SCHEMA_REGISTRY_FEATURE_REQUEST = gql(`
|
||||
mutation addSchemaRegistryFeatureRequest ($details:jsonb!) {
|
||||
addFeatureRequest (payload: {
|
||||
type: "schema-registry-feature-request"
|
||||
details: $details
|
||||
}) {
|
||||
status
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import { RoleBasedSchema } from '../types';
|
||||
|
||||
export const ChangeSummary: React.VFC<{
|
||||
changes: RoleBasedSchema['changes'];
|
||||
}> = props => {
|
||||
const { changes } = props;
|
||||
|
||||
if (!changes) {
|
||||
return <span>Unknown</span>;
|
||||
}
|
||||
|
||||
const numBreakingChanges = changes.filter(
|
||||
c => c.criticality.level === 'BREAKING'
|
||||
).length;
|
||||
const numDangerousChanges = changes.filter(
|
||||
c => c.criticality.level === 'DANGEROUS'
|
||||
).length;
|
||||
const numSafeChanges = changes.filter(
|
||||
c => c.criticality.level === 'NON_BREAKING'
|
||||
).length;
|
||||
|
||||
if (
|
||||
numBreakingChanges === 0 &&
|
||||
numDangerousChanges === 0 &&
|
||||
numSafeChanges === 0
|
||||
) {
|
||||
return <span>No changes detected</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-1/5">
|
||||
<div className="flex-col">
|
||||
<div className="flex text-red-600 text-2xl font-bold">
|
||||
{numBreakingChanges}
|
||||
</div>
|
||||
<span>Breaking</span>
|
||||
</div>
|
||||
<div className="flex-col">
|
||||
<div className="flex text-red-800 text-2xl font-bold">
|
||||
{numDangerousChanges}
|
||||
</div>
|
||||
<span>Dangerous</span>
|
||||
</div>
|
||||
<div className="flex-col">
|
||||
<div className="flex text-green-600 text-2xl font-bold">
|
||||
{numSafeChanges}
|
||||
</div>
|
||||
<span>Safe</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import * as React from 'react';
|
||||
import { SchemasList } from './SchemasList';
|
||||
import { FeatureRequest } from './FeatureRequest';
|
||||
import globals from '../../../Globals';
|
||||
import { SCHEMA_REGISTRY_FEATURE_NAME } from '../constants';
|
||||
|
||||
const Header: React.VFC = () => {
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
<h1 className="text-xl font-semibold">GraphQL Schema Registry</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Body: React.VFC<{ hasFeatureAccess: boolean }> = props => {
|
||||
const { hasFeatureAccess } = props;
|
||||
|
||||
if (!hasFeatureAccess) {
|
||||
return <FeatureRequest />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
<SchemasList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SchemaRegistryContainer: React.VFC = () => {
|
||||
const hasFeatureAccess = globals.allowedLuxFeatures.includes(
|
||||
SCHEMA_REGISTRY_FEATURE_NAME
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4 flex flex-col w-full">
|
||||
<div className="flex w-full mb-md">
|
||||
<Header />
|
||||
</div>
|
||||
<div className="flex w-full mb-md">
|
||||
<Body hasFeatureAccess={hasFeatureAccess} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { ChangeLevel } from '../types';
|
||||
|
||||
export const CountLabel: React.VFC<{
|
||||
count: number;
|
||||
type: ChangeLevel;
|
||||
}> = props => {
|
||||
const { count, type } = props;
|
||||
|
||||
let textColor = 'text-green-500';
|
||||
let backgroundColor = 'bg-green-100';
|
||||
|
||||
switch (type) {
|
||||
case 'BREAKING': {
|
||||
textColor = 'text-red-500';
|
||||
backgroundColor = 'bg-red-100';
|
||||
break;
|
||||
}
|
||||
case 'DANGEROUS': {
|
||||
textColor = 'text-yellow-500';
|
||||
backgroundColor = 'bg-yellow-100';
|
||||
break;
|
||||
}
|
||||
case 'NON_BREAKING': {
|
||||
textColor = 'text-green-500';
|
||||
backgroundColor = 'bg-green-100';
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded ${backgroundColor}`}>
|
||||
<span className={`font-bold ${textColor} mx-2`}>{count}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,75 @@
|
||||
import * as React from 'react';
|
||||
import { Button } from '../../../new-components/Button';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { hasuraToast } from '../../../new-components/Toasts';
|
||||
import { useSubmitSchemaRegistryFeatureRequest } from '../hooks/useSubmitSchemaRegistryFeatureRequest';
|
||||
|
||||
export const FeatureRequest = () => {
|
||||
const onSuccess = () => {
|
||||
hasuraToast({
|
||||
title: 'We have received your feature request for Schema Registry.',
|
||||
message: 'Our team will enable the feature for you soon.',
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const onError = (error?: GraphQLError) => {
|
||||
const errorMessage = error?.message || '';
|
||||
if (errorMessage.includes('request already submitted')) {
|
||||
hasuraToast({
|
||||
title: 'We have received your feature request for Schema Registry.',
|
||||
message: 'Our team will enable the feature for you soon.',
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
hasuraToast({
|
||||
title: 'Error requesting feature access',
|
||||
message: 'Please try again in some time.',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { onSubmit, loading } = useSubmitSchemaRegistryFeatureRequest(
|
||||
onSuccess,
|
||||
onError
|
||||
);
|
||||
|
||||
const onRequest = () => {
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white w-[50%] p-4 border-neutral-200 semi-rounded">
|
||||
<span className="mb-sm text-muted">
|
||||
The Hasura Schema Registry work is aimed to make your Hasura GraphQL
|
||||
schema changes more reliable, prevent breaking changes in your schema
|
||||
and make collaboration across large teams, micro services and roles much
|
||||
more manageable and predictable.{' '}
|
||||
<a href="" target="_blank" rel="noreferrer noopener">
|
||||
{' '}
|
||||
Read more.
|
||||
</a>
|
||||
</span>
|
||||
<div className="mb-sm">
|
||||
<img
|
||||
src="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-console/assets/common/img/schema-registry-preview-screenshot.png"
|
||||
alt="Schema Registry Preview Image"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full items-center">
|
||||
<Button
|
||||
mode="primary"
|
||||
className={`mr-sm ${loading ? 'cursor-not-allowed' : ''}`}
|
||||
disabled={loading}
|
||||
onClick={onRequest}
|
||||
>
|
||||
{loading ? 'Requesting...' : 'Request access'}
|
||||
</Button>
|
||||
<h4 className="font-italics text-muted">
|
||||
This feature is currently in closed alpha.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,241 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import LZString from 'lz-string';
|
||||
import moment from 'moment';
|
||||
import { useGetSchema } from '../hooks/useGetSchema';
|
||||
import { Tabs } from '../../../new-components/Tabs';
|
||||
import { SchemaRow } from './SchemaRow';
|
||||
import { ChangeSummary } from './ChangeSummary';
|
||||
import { FindIfSubStringExists, schemaTransformFn } from '../utils';
|
||||
import { Link } from 'react-router';
|
||||
import { RoleBasedSchema, Schema } from '../types';
|
||||
import { FaHome, FaAngleRight, FaFileImport, FaSearch } from 'react-icons/fa';
|
||||
import { Input } from '../../../new-components/Form';
|
||||
import AceEditor from 'react-ace';
|
||||
|
||||
export const Breadcrumbs = () => (
|
||||
<div className="flex items-center space-x-xs mb-4">
|
||||
<Link
|
||||
to="/settings/schema-registry"
|
||||
className="cursor-pointer flex items-center text-muted hover:text-gray-900"
|
||||
>
|
||||
<FaHome className="mr-1.5" />
|
||||
<span className="text-sm">Schema</span>
|
||||
</Link>
|
||||
<FaAngleRight className="text-muted" />
|
||||
<div className="cursor-pointer flex items-center text-yellow-500">
|
||||
<FaFileImport className="mr-1.5" />
|
||||
<span className="text-sm">Roles</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type SchemaDetailsViewProps = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const SchemaDetailsView = (props: SchemaDetailsViewProps) => {
|
||||
const { id: schemaId } = props.params;
|
||||
const fetchSchemaResponse = useGetSchema(schemaId);
|
||||
|
||||
const { kind } = fetchSchemaResponse;
|
||||
|
||||
switch (kind) {
|
||||
case 'loading':
|
||||
return <p>Loading...</p>;
|
||||
case 'error':
|
||||
return <p>Error: {fetchSchemaResponse.message}</p>;
|
||||
case 'success': {
|
||||
const transformedData = schemaTransformFn(fetchSchemaResponse.response);
|
||||
return <SchemasDetails schema={transformedData} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const SchemasDetails: React.VFC<{
|
||||
schema: Schema;
|
||||
}> = props => {
|
||||
const { schema } = props;
|
||||
|
||||
const [tabState, setTabState] = React.useState('graphql');
|
||||
|
||||
// We know for sure only one roleBasedSchema exists
|
||||
const roleBasedSchema = schema.roleBasedSchemas[0];
|
||||
const published = moment(schema.created_at);
|
||||
|
||||
return (
|
||||
<div className="mx-4 mt-4">
|
||||
<Breadcrumbs />
|
||||
<div className="flex mb-sm">
|
||||
<span className="font-bold text-xl text-black mr-4">
|
||||
{roleBasedSchema.role}:{schema.entry_hash.substr(0, 16)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-neutral-200 bg-white border w-3/5">
|
||||
<div className="w-full flex bg-gray-100 px-4 py-2">
|
||||
<div className="flex text-base w-[69%] justify-start">
|
||||
<span className="text-sm font-bold">SCHEMA</span>
|
||||
</div>
|
||||
<div className="flex text-base w-[28%] justify-between">
|
||||
<span className="text-sm font-bold">BREAKING</span>
|
||||
<span className="text-sm font-bold">DANGEROUS</span>
|
||||
<span className="text-sm font-bold">SAFE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-4 mb-2 ">
|
||||
<SchemaRow
|
||||
role={roleBasedSchema.role || ''}
|
||||
changes={roleBasedSchema.changes || []}
|
||||
/>
|
||||
|
||||
<div className="flex mt-4">
|
||||
<div className="flex-col w-1/4">
|
||||
<div className="font-bold text-gray-500">Published</div>
|
||||
<span>{published.fromNow()}</span>
|
||||
</div>
|
||||
<div className="flex-col w-3/4">
|
||||
<div className="font-bold text-gray-500">Hash</div>
|
||||
<span className="font-bold bg-gray-100 px-1 rounded text-sm">
|
||||
{roleBasedSchema.hash}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full">
|
||||
<Tabs
|
||||
value={tabState}
|
||||
onValueChange={state => setTabState(state)}
|
||||
headerTabBackgroundColor="bg-white"
|
||||
items={[
|
||||
{
|
||||
value: 'graphql',
|
||||
label: 'GraphQL',
|
||||
content: <SchemaView schema={roleBasedSchema.raw} />,
|
||||
},
|
||||
{
|
||||
value: 'changes',
|
||||
label: 'Changes',
|
||||
content: <ChangesView changes={roleBasedSchema.changes} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SchemaView: React.VFC<{ schema: string }> = props => {
|
||||
const { schema } = props;
|
||||
const decompressedSchema = LZString.decompressFromBase64(schema);
|
||||
return (
|
||||
<div className="w-full p-sm">
|
||||
<AceEditor
|
||||
mode="graphqlschema"
|
||||
fontSize={14}
|
||||
width="100%"
|
||||
theme="chrome"
|
||||
name={`schema-registry-schema-modal-view-schema`}
|
||||
value={decompressedSchema}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{ useWorker: false }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChangesView: React.VFC<{
|
||||
changes: RoleBasedSchema['changes'];
|
||||
}> = props => {
|
||||
const { changes } = props;
|
||||
|
||||
const [searchText, setSearchText] = React.useState('');
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchText(e.target.value);
|
||||
|
||||
const changesList = useMemo(() => {
|
||||
if (!searchText) return changes;
|
||||
|
||||
return changes?.filter(change =>
|
||||
FindIfSubStringExists(change.message, searchText)
|
||||
);
|
||||
}, [searchText, changes]);
|
||||
|
||||
if (!changes) {
|
||||
return <span>Could not compute what changed in this GraphQL Schema</span>;
|
||||
}
|
||||
|
||||
if (!changes.length) {
|
||||
return <span className="ml-4">No changes!</span>;
|
||||
}
|
||||
|
||||
const breakingChanges =
|
||||
changesList && changesList.filter(c => c.criticality.level === 'BREAKING');
|
||||
const dangerousChanges =
|
||||
changesList && changesList.filter(c => c.criticality.level === 'DANGEROUS');
|
||||
const safeChanges =
|
||||
changesList &&
|
||||
changesList.filter(c => c.criticality.level === 'NON_BREAKING');
|
||||
|
||||
return (
|
||||
<div className="flex-col m-8">
|
||||
<div className="flex-col">
|
||||
<div className="font-bold text-md mb-8 text-gray-500">
|
||||
Change Summary
|
||||
</div>
|
||||
<ChangeSummary changes={changes} />
|
||||
</div>
|
||||
<div className="flex w-full border-b border-gray-300 my-8" />
|
||||
<div className="flex w-full mb-4 justify-between">
|
||||
<div className="flex font-bold text-gray-600">Changes</div>
|
||||
|
||||
<label className="block">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
name="search"
|
||||
icon={<FaSearch />}
|
||||
iconPosition="start"
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{breakingChanges && breakingChanges.length > 0 && (
|
||||
<div className="flex flex-col">
|
||||
{breakingChanges.map(c => {
|
||||
return (
|
||||
<div className="text-red-600 border border-gray-400 p-2">
|
||||
{c.message}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{dangerousChanges && dangerousChanges.length > 0 && (
|
||||
<div className="flex flex-col">
|
||||
{dangerousChanges.map(c => {
|
||||
return (
|
||||
<div className="text-red-800 border border-gray-400 p-2">
|
||||
{c.message}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{safeChanges && safeChanges.length > 0 && (
|
||||
<div className="flex flex-col">
|
||||
{safeChanges.map(c => {
|
||||
return (
|
||||
<div className="text-green-600 border border-gray-400 p-2">
|
||||
{c.message}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,128 @@
|
||||
import * as React from 'react';
|
||||
import { Dialog } from '../../../new-components/Dialog';
|
||||
import { Tabs } from '../../../new-components/Tabs';
|
||||
import { Schema } from '../types';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/html';
|
||||
import 'brace/mode/markdown';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/theme/chrome';
|
||||
|
||||
type Props = {
|
||||
onClose: VoidFunction;
|
||||
schema: Schema | null;
|
||||
};
|
||||
|
||||
export const SchemaModal: React.VFC<Props> = props => {
|
||||
const [tabState, setTabState] = React.useState('schema');
|
||||
|
||||
const { schema, onClose } = props;
|
||||
|
||||
if (!schema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
hasBackdrop
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
title="GraphQL Schema Details"
|
||||
description=""
|
||||
>
|
||||
<div className="w-full h-full p-md">
|
||||
<Tabs
|
||||
value={tabState}
|
||||
onValueChange={state => setTabState(state)}
|
||||
items={[
|
||||
{
|
||||
value: 'schema',
|
||||
label: 'GraphQL Schema',
|
||||
content: <SchemaView schema={schema.raw} />,
|
||||
},
|
||||
{
|
||||
value: 'diff',
|
||||
label: 'Changes',
|
||||
content: <DiffView changes={schema.changes} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const SchemaView: React.VFC<{ schema: string }> = props => {
|
||||
const { schema } = props;
|
||||
return (
|
||||
<div className="w-full p-sm">
|
||||
<AceEditor
|
||||
mode="graphql"
|
||||
fontSize={14}
|
||||
width="100%"
|
||||
theme="github"
|
||||
name={`schema-registry-schema-modal-view-schema`}
|
||||
value={schema}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{ useWorker: false }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DiffView: React.VFC<{ changes: Schema['changes'] }> = props => {
|
||||
const { changes } = props;
|
||||
|
||||
if (!changes) {
|
||||
return <span>Could not compute what changed in this GraphQL Schema</span>;
|
||||
}
|
||||
|
||||
if (!changes.length) {
|
||||
return <span>No changes!</span>;
|
||||
}
|
||||
|
||||
const breakingChanges = changes.filter(
|
||||
c => c.criticality.level === 'BREAKING'
|
||||
);
|
||||
const dangerousChanges = changes.filter(
|
||||
c => c.criticality.level === 'DANGEROUS'
|
||||
);
|
||||
const safeChanges = changes.filter(
|
||||
c => c.criticality.level === 'NON_BREAKING'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full p-sm">
|
||||
{breakingChanges.length && (
|
||||
<div className="flex flex-col w-full mb-sm">
|
||||
<b className="mb-xs">Breaking Changes</b>
|
||||
<ul className="marker:text-red-600 list-outside list-disc ml-6">
|
||||
{breakingChanges.map(c => {
|
||||
return <li>{c.message}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{dangerousChanges.length && (
|
||||
<div className="flex flex-col w-full mb-sm">
|
||||
<b className="mb-xs">Dangerous Changes</b>
|
||||
<ul className="marker:text-yellow-500 list-outside list-disc ml-6">
|
||||
{dangerousChanges.map(c => {
|
||||
return <li>{c.message}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{safeChanges.length && (
|
||||
<div className="flex flex-col w-full mb-sm">
|
||||
<b className="mb-xs">Safe Changes</b>
|
||||
<ul className="marker:text-lime-500 list-outside list-disc ml-6">
|
||||
{safeChanges.map(c => {
|
||||
return <li>{c.message}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { CountLabel } from './CountLabel';
|
||||
import { CapitalizeFirstLetter } from '../utils';
|
||||
import { SchemaChange } from '../types';
|
||||
import { FaChevronRight } from 'react-icons/fa';
|
||||
|
||||
export const SchemaRow: React.VFC<{
|
||||
role: string;
|
||||
changes: SchemaChange[];
|
||||
onClick?: VoidFunction;
|
||||
}> = props => {
|
||||
const { role, changes, onClick } = props;
|
||||
|
||||
const countBreakingChanges = changes?.filter(
|
||||
c => c.criticality.level === 'BREAKING'
|
||||
).length;
|
||||
const countDangerousChanges = changes?.filter(
|
||||
c => c.criticality.level === 'DANGEROUS'
|
||||
).length;
|
||||
const countSafeChanges = changes?.filter(
|
||||
c => c.criticality.level === 'NON_BREAKING'
|
||||
).length;
|
||||
return (
|
||||
<div className="w-full flex my-2">
|
||||
<div className="flex text-base w-[72%] justify-start">
|
||||
<span className="text-sm font-bold bg-gray-100 p-1 rounded">
|
||||
{CapitalizeFirstLetter(role)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex text-base w-[28%] justify-between">
|
||||
<CountLabel count={countBreakingChanges} type="BREAKING" />
|
||||
<CountLabel count={countDangerousChanges} type="DANGEROUS" />
|
||||
<CountLabel count={countSafeChanges} type="NON_BREAKING" />
|
||||
</div>
|
||||
{onClick && (
|
||||
<div
|
||||
className="flex text-base w-[2%] justify-end mt-[6px]"
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<FaChevronRight />
|
||||
</div>
|
||||
)}
|
||||
{!onClick && <div className="flex w-[4%] justify-end"></div>}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import { SchemaRow } from './SchemaRow';
|
||||
import { useGetSchemaList } from '../hooks/useGetSchemaList';
|
||||
import { Schema, RoleBasedSchema } from '../types';
|
||||
import moment from 'moment';
|
||||
import { browserHistory } from 'react-router';
|
||||
import globals from '../../../Globals';
|
||||
import { schemaListTransformFn } from '../utils';
|
||||
|
||||
export const SchemasList = () => {
|
||||
const projectID = globals.hasuraCloudProjectId || '';
|
||||
const fetchSchemaResponse = useGetSchemaList(projectID);
|
||||
|
||||
const { kind } = fetchSchemaResponse;
|
||||
|
||||
switch (kind) {
|
||||
case 'loading':
|
||||
return <p>Loading...</p>;
|
||||
case 'error':
|
||||
return <p>Error: {fetchSchemaResponse.message}</p>;
|
||||
case 'success': {
|
||||
const schemaList = schemaListTransformFn(fetchSchemaResponse.response);
|
||||
|
||||
return <Tabularised schemas={schemaList} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Tabularised: React.VFC<{ schemas: Schema[] }> = props => {
|
||||
const { schemas } = props;
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-sm border-neutral-200 bg-gray-100 border w-3/5">
|
||||
<div className="w-full flex bg-gray-100 px-4 py-2">
|
||||
<div className="flex text-base w-[69%] justify-start">
|
||||
<span className="text-sm font-bold">SCHEMA</span>
|
||||
</div>
|
||||
<div className="flex text-base w-[28%] justify-between">
|
||||
<span className="text-sm font-bold">BREAKING</span>
|
||||
<span className="text-sm font-bold">DANGEROUS</span>
|
||||
<span className="text-sm font-bold">SAFE</span>
|
||||
</div>
|
||||
{/* <div className="flex text-base w-[2%] justify-end">he</div> */}
|
||||
</div>
|
||||
<div className="flex flex-col w-full">
|
||||
{schemas.length ? (
|
||||
schemas.map(schema => {
|
||||
return (
|
||||
<SchemaCard
|
||||
createdAt={schema.created_at}
|
||||
schemaId={schema.id}
|
||||
hash={schema.hash}
|
||||
roleBasedSchemas={schema.roleBasedSchemas}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="white border-t border-neutral-200">
|
||||
<div className="p-xs" data-test="label-no-domain-found">
|
||||
No schemas published to the schema registry yet
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SchemaCard: React.VFC<{
|
||||
createdAt: string;
|
||||
hash: string;
|
||||
schemaId: string;
|
||||
roleBasedSchemas: RoleBasedSchema[];
|
||||
}> = props => {
|
||||
const { createdAt, hash, roleBasedSchemas } = props;
|
||||
|
||||
const published = moment(createdAt);
|
||||
|
||||
return (
|
||||
<div className="w-full flex-col px-4 py-2 mb-2 bg-white">
|
||||
<div className="flex flex-col w-1/2">
|
||||
<div className="flex mt-4">
|
||||
<div className="flex-col w-1/4">
|
||||
<div className="font-bold text-gray-500">Published</div>
|
||||
<span>{published.fromNow()}</span>
|
||||
</div>
|
||||
<div className="flex-col w-3/4">
|
||||
<div className="font-bold text-gray-500">Hash</div>
|
||||
<span className="font-bold bg-gray-100 px-1 rounded text-sm">
|
||||
{hash}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-col w-full mt-8">
|
||||
{roleBasedSchemas.length ? (
|
||||
roleBasedSchemas.map((roleBasedSchema, index) => {
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<SchemaRow
|
||||
role={roleBasedSchema.role || ''}
|
||||
changes={roleBasedSchema.changes || []}
|
||||
onClick={() => {
|
||||
browserHistory.push(
|
||||
`${globals.urlPrefix}/settings/schema-registry/${roleBasedSchema.id}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{!(index + 1 === roleBasedSchemas.length) ? (
|
||||
<div className="flex w-full border-b border-gray-300" />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="white border-t border-neutral-200">
|
||||
<div className="p-xs" data-test="label-no-domain-found">
|
||||
No schemas published to the schema registry yet
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
export const FETCH_REGISTRY_SCHEMAS_QUERY_NAME =
|
||||
'FETCH_REGISTRY_SCHEMAS_QUERY_NAME';
|
||||
export const FETCH_REGISTRY_SCHEMA_QUERY_NAME =
|
||||
'FETCH_REGISTRY_SCHEMA_QUERY_NAME';
|
||||
|
||||
// 5 minutes as default stale time
|
||||
export const SCHEMA_REGISTRY_REFRESH_TIME = 5 * 60 * 1000;
|
||||
|
||||
export const SCHEMA_REGISTRY_FEATURE_NAME = 'GraphQLSchemaRegistry';
|
@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { schemaRegsitryControlPlaneClient } from '../utils';
|
||||
import { FETCH_REGSITRY_SCHEMA_QUERY } from '../queries';
|
||||
import { GetRegistrySchemaResponseWithError } from '../types';
|
||||
import {
|
||||
FETCH_REGISTRY_SCHEMA_QUERY_NAME,
|
||||
SCHEMA_REGISTRY_REFRESH_TIME,
|
||||
} from '../constants';
|
||||
|
||||
type FetchSchemaResponse =
|
||||
| {
|
||||
kind: 'loading';
|
||||
}
|
||||
| {
|
||||
kind: 'error';
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
kind: 'success';
|
||||
response: NonNullable<GetRegistrySchemaResponseWithError['data']>;
|
||||
};
|
||||
|
||||
export const useGetSchema = (schemaId: string): FetchSchemaResponse => {
|
||||
const fetchRegistrySchemaQueryFn = (schemaId: string) => {
|
||||
return schemaRegsitryControlPlaneClient.query<
|
||||
GetRegistrySchemaResponseWithError,
|
||||
{ schemaId: string }
|
||||
>(FETCH_REGSITRY_SCHEMA_QUERY, {
|
||||
schemaId: schemaId,
|
||||
});
|
||||
};
|
||||
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: FETCH_REGISTRY_SCHEMA_QUERY_NAME,
|
||||
queryFn: () => fetchRegistrySchemaQueryFn(schemaId),
|
||||
refetchOnMount: 'always',
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: SCHEMA_REGISTRY_REFRESH_TIME,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return {
|
||||
kind: 'loading',
|
||||
};
|
||||
}
|
||||
|
||||
if (error || !data || !!data?.errors || !data?.data) {
|
||||
return {
|
||||
kind: 'error',
|
||||
message: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'success',
|
||||
response: data.data,
|
||||
};
|
||||
};
|
@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { FETCH_REGSITRY_SCHEMAS_QUERY } from '../queries';
|
||||
import { schemaRegsitryControlPlaneClient } from '../utils';
|
||||
import { GetSchemaListResponseWithError } from '../types';
|
||||
import {
|
||||
FETCH_REGISTRY_SCHEMAS_QUERY_NAME,
|
||||
SCHEMA_REGISTRY_REFRESH_TIME,
|
||||
} from '../constants';
|
||||
|
||||
type FetchSchemaResponse =
|
||||
| {
|
||||
kind: 'loading';
|
||||
}
|
||||
| {
|
||||
kind: 'error';
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
kind: 'success';
|
||||
response: NonNullable<GetSchemaListResponseWithError['data']>;
|
||||
};
|
||||
|
||||
export const useGetSchemaList = (projectId: string): FetchSchemaResponse => {
|
||||
const fetchRegistrySchemasQueryFn = (projectId: string) => {
|
||||
return schemaRegsitryControlPlaneClient.query<
|
||||
GetSchemaListResponseWithError,
|
||||
{ projectId: string }
|
||||
>(FETCH_REGSITRY_SCHEMAS_QUERY, {
|
||||
projectId: projectId,
|
||||
});
|
||||
};
|
||||
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: FETCH_REGISTRY_SCHEMAS_QUERY_NAME,
|
||||
queryFn: () => fetchRegistrySchemasQueryFn(projectId),
|
||||
refetchOnMount: 'always',
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: SCHEMA_REGISTRY_REFRESH_TIME,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return {
|
||||
kind: 'loading',
|
||||
};
|
||||
}
|
||||
|
||||
if (error || !data || !!data.errors || !data.data) {
|
||||
return {
|
||||
kind: 'error',
|
||||
message: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'success',
|
||||
response: data.data,
|
||||
};
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import { useMutation } from 'react-query';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import globals from '../../../Globals';
|
||||
import {
|
||||
controlPlaneClient,
|
||||
ADD_SCHEMA_REGISTRY_FEATURE_REQUEST,
|
||||
AddSchemaRegistryFeatureRequestMutation,
|
||||
} from '../../ControlPlane';
|
||||
|
||||
export const useSubmitSchemaRegistryFeatureRequest = (
|
||||
successCb: () => void,
|
||||
errorCb: (error?: GraphQLError) => void
|
||||
) => {
|
||||
const addSchemaRegistryFtRequestMutation = () => {
|
||||
return controlPlaneClient.query<{
|
||||
data: AddSchemaRegistryFeatureRequestMutation;
|
||||
errors: [GraphQLError];
|
||||
}>(ADD_SCHEMA_REGISTRY_FEATURE_REQUEST, {
|
||||
details: {
|
||||
source: 'cloud-console',
|
||||
project_id: globals.hasuraCloudProjectId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mutation = useMutation(addSchemaRegistryFtRequestMutation, {
|
||||
onSuccess: data => {
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
errorCb(data.errors[0]);
|
||||
} else {
|
||||
successCb();
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
errorCb();
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = () => {
|
||||
mutation.mutate();
|
||||
};
|
||||
|
||||
return {
|
||||
onSubmit,
|
||||
loading: mutation.isLoading,
|
||||
};
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export { SchemaRegistryContainer } from './components/Container';
|
||||
export { SchemaDetailsView } from './components/SchemaDetails';
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,58 @@
|
||||
import { parse as gql } from 'graphql';
|
||||
|
||||
export const FETCH_REGSITRY_SCHEMAS_QUERY = gql(`
|
||||
query fetchRegistrySchemas($projectId: uuid!) {
|
||||
schema_registry_dumps(
|
||||
where: {_and: [{project_id: {_eq: $projectId}, hasura_schema_role: {_eq: "admin"}}]},
|
||||
order_by: {change_recorded_at: desc}
|
||||
) {
|
||||
change_recorded_at
|
||||
schema_hash
|
||||
entry_hash
|
||||
id
|
||||
sibling_schemas {
|
||||
id
|
||||
entry_hash
|
||||
change_recorded_at
|
||||
created_at
|
||||
hasura_schema_role
|
||||
schema_sdl
|
||||
diff_with_previous_schema {
|
||||
current_schema_hash
|
||||
former_schema_hash
|
||||
former_schema_id
|
||||
current_schema_id
|
||||
schema_diff_data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
`);
|
||||
|
||||
export const FETCH_REGSITRY_SCHEMA_QUERY = gql(`
|
||||
query fetchRegistrySchema ($schemaId: uuid!) {
|
||||
schema_registry_dumps (
|
||||
where: {
|
||||
id: {
|
||||
_eq: $schemaId
|
||||
}
|
||||
}
|
||||
) {
|
||||
id
|
||||
entry_hash
|
||||
schema_hash
|
||||
change_recorded_at
|
||||
created_at
|
||||
hasura_schema_role
|
||||
schema_sdl
|
||||
diff_with_previous_schema {
|
||||
current_schema_hash
|
||||
former_schema_hash
|
||||
former_schema_id
|
||||
current_schema_id
|
||||
schema_diff_data
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
@ -0,0 +1,88 @@
|
||||
import { GraphQLError } from 'graphql';
|
||||
|
||||
export type SafeSchemaChange = 'NON_BREAKING';
|
||||
export type DangerousSchemaChange = 'DANGEROUS';
|
||||
export type BreakingSchemaChange = 'BREAKING';
|
||||
|
||||
export type ChangeLevel =
|
||||
| DangerousSchemaChange
|
||||
| BreakingSchemaChange
|
||||
| SafeSchemaChange;
|
||||
|
||||
export type SchemaChange = {
|
||||
criticality: {
|
||||
level: ChangeLevel;
|
||||
reason?: string;
|
||||
};
|
||||
type: string;
|
||||
meta: any;
|
||||
message: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type RoleBasedSchema = {
|
||||
raw: string;
|
||||
role: string;
|
||||
entry_hash: string;
|
||||
hash: string;
|
||||
id: string;
|
||||
changes?: SchemaChange[];
|
||||
};
|
||||
|
||||
export type Schema = {
|
||||
id: string;
|
||||
hash: string;
|
||||
entry_hash: string;
|
||||
created_at: string;
|
||||
roleBasedSchemas: RoleBasedSchema[];
|
||||
};
|
||||
|
||||
/*
|
||||
Schema Registry Query types
|
||||
*/
|
||||
|
||||
export type GetSchemaListResponseWithError = {
|
||||
data?: GetSchemaListQueryResponse;
|
||||
errors?: GraphQLError[];
|
||||
};
|
||||
|
||||
export type GetSchemaListQueryResponse = {
|
||||
schema_registry_dumps: SchemaRegistryDumpWithSiblingSchema[];
|
||||
};
|
||||
|
||||
export type SchemaRegistryDump = {
|
||||
id: string;
|
||||
entry_hash: string;
|
||||
schema_hash: string;
|
||||
change_recorded_at: string;
|
||||
created_at: string;
|
||||
hasura_schema_role: string;
|
||||
schema_sdl: string;
|
||||
};
|
||||
|
||||
export type SchemaRegistryDumpWithSiblingSchema = SchemaRegistryDump & {
|
||||
sibling_schemas: SiblingSchema[];
|
||||
};
|
||||
|
||||
export type SiblingSchema = SchemaRegistryDump & {
|
||||
diff_with_previous_schema: SchemaDiffData[];
|
||||
};
|
||||
|
||||
export type SchemaDiffData = {
|
||||
current_schema_hash: string;
|
||||
former_schema_hash: string;
|
||||
former_schema_id: string;
|
||||
current_schema_id: string;
|
||||
// For the following, using the same type (SchemaChange) used for UI, if ever the query response type changes
|
||||
// a refactor in UI will also be required thus changing the inherent type
|
||||
schema_diff_data: SchemaChange[];
|
||||
};
|
||||
|
||||
export type GetRegistrySchemaResponseWithError = {
|
||||
data?: GetRegistrySchemaQueryResponse;
|
||||
errors?: GraphQLError[];
|
||||
};
|
||||
|
||||
export type GetRegistrySchemaQueryResponse = {
|
||||
schema_registry_dumps: SiblingSchema[];
|
||||
};
|
@ -0,0 +1,102 @@
|
||||
import { createControlPlaneClient } from '../ControlPlane';
|
||||
import endpoints from '../../Endpoints';
|
||||
import {
|
||||
Schema,
|
||||
RoleBasedSchema,
|
||||
SchemaChange,
|
||||
GetSchemaListResponseWithError,
|
||||
SchemaRegistryDumpWithSiblingSchema,
|
||||
SiblingSchema,
|
||||
GetRegistrySchemaResponseWithError,
|
||||
} from './types';
|
||||
|
||||
export const CapitalizeFirstLetter = (str: string) => {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
export const FindIfSubStringExists = (
|
||||
originalString: string,
|
||||
subString: string
|
||||
) => {
|
||||
return originalString.toLowerCase().includes(subString.toLocaleLowerCase());
|
||||
};
|
||||
|
||||
export const schemaRegsitryLuxDataEndpoint = endpoints.schemaRegistry;
|
||||
|
||||
export const schemaRegsitryControlPlaneClient = createControlPlaneClient(
|
||||
schemaRegsitryLuxDataEndpoint,
|
||||
{}
|
||||
);
|
||||
|
||||
export const schemaListTransformFn = (
|
||||
fetchedData: NonNullable<GetSchemaListResponseWithError['data']>
|
||||
) => {
|
||||
const dumps = fetchedData.schema_registry_dumps || [];
|
||||
|
||||
const schemaList: Schema[] = [];
|
||||
|
||||
dumps.forEach((dump: SchemaRegistryDumpWithSiblingSchema) => {
|
||||
const roleBasedSchemas: RoleBasedSchema[] = [];
|
||||
|
||||
dump.sibling_schemas.forEach((childSchema: SiblingSchema) => {
|
||||
const changes: SchemaChange[] = [
|
||||
...(childSchema.diff_with_previous_schema?.[0]?.schema_diff_data || []),
|
||||
];
|
||||
|
||||
const roleBasedSchema: RoleBasedSchema = {
|
||||
raw: childSchema.schema_sdl,
|
||||
role: childSchema.hasura_schema_role,
|
||||
hash: childSchema.schema_hash,
|
||||
entry_hash: dump.entry_hash,
|
||||
id: childSchema.id,
|
||||
changes: changes,
|
||||
};
|
||||
|
||||
roleBasedSchemas.push(roleBasedSchema);
|
||||
});
|
||||
|
||||
const schema: Schema = {
|
||||
hash: dump.schema_hash,
|
||||
created_at: dump.change_recorded_at,
|
||||
id: dump.id,
|
||||
entry_hash: dump.entry_hash,
|
||||
roleBasedSchemas: roleBasedSchemas,
|
||||
};
|
||||
|
||||
schemaList.push(schema);
|
||||
});
|
||||
return schemaList;
|
||||
};
|
||||
|
||||
export const schemaTransformFn = (
|
||||
fetchedData: NonNullable<GetRegistrySchemaResponseWithError['data']>
|
||||
) => {
|
||||
const data = fetchedData.schema_registry_dumps[0] || [];
|
||||
|
||||
const roleBasedSchemas: RoleBasedSchema[] = [];
|
||||
|
||||
const changes = [
|
||||
...(data.diff_with_previous_schema?.[0]?.schema_diff_data || []),
|
||||
];
|
||||
|
||||
const roleBasedSchema: RoleBasedSchema = {
|
||||
id: data.id,
|
||||
hash: data.schema_hash,
|
||||
raw: data.schema_sdl,
|
||||
role: data.hasura_schema_role,
|
||||
entry_hash: data.entry_hash,
|
||||
changes: changes,
|
||||
};
|
||||
|
||||
roleBasedSchemas.push(roleBasedSchema);
|
||||
|
||||
const schema: Schema = {
|
||||
hash: data.schema_hash,
|
||||
entry_hash: data.entry_hash,
|
||||
created_at: data.change_recorded_at,
|
||||
id: data.id,
|
||||
roleBasedSchemas: roleBasedSchemas,
|
||||
};
|
||||
|
||||
return schema;
|
||||
};
|
@ -9,19 +9,26 @@ interface TabsItem {
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
interface TabsProps extends React.ComponentProps<typeof RadixTabs.Root> {
|
||||
interface TabsCustomProps extends React.ComponentProps<typeof RadixTabs.Root> {
|
||||
headerTabBackgroundColor?: string;
|
||||
}
|
||||
interface TabsProps extends TabsCustomProps {
|
||||
items: TabsItem[];
|
||||
}
|
||||
|
||||
export const Tabs: React.FC<TabsProps> = props => {
|
||||
const { items, ...rest } = props;
|
||||
const { headerTabBackgroundColor, items, ...rest } = props;
|
||||
|
||||
const backgroundColor = headerTabBackgroundColor ?? 'bg-legacybg';
|
||||
return (
|
||||
<RadixTabs.Root
|
||||
defaultValue={rest?.defaultValue ?? items[0]?.value}
|
||||
{...rest}
|
||||
>
|
||||
<RadixTabs.List aria-label="Tabs">
|
||||
<div className="border-b border-gray-200 bg-legacybg flex space-x-4">
|
||||
<div
|
||||
className={`border-b border-gray-200 ${backgroundColor} flex space-x-4`}
|
||||
>
|
||||
{items.map(({ value: itemValue, label, icon }) => (
|
||||
<RadixTabs.Trigger key={itemValue} value={itemValue} asChild>
|
||||
<button
|
||||
|
@ -51,6 +51,8 @@ import {
|
||||
MultipleAdminSecretsPage,
|
||||
MultipleJWTSecretsPage,
|
||||
SingleSignOnPage,
|
||||
SchemaRegistryContainer,
|
||||
SchemaDetailsView,
|
||||
} from '@hasura/console-legacy-ce';
|
||||
|
||||
import AccessDeniedComponent from './components/AccessDenied/AccessDenied';
|
||||
@ -369,6 +371,8 @@ const routes = store => {
|
||||
>
|
||||
<Route path="settings" component={metadataContainer(connect)}>
|
||||
<IndexRedirect to="metadata-actions" />
|
||||
<Route path="schema-registry" component={SchemaRegistryContainer} />
|
||||
<Route path="schema-registry/:id" component={SchemaDetailsView} />
|
||||
<Route
|
||||
path="metadata-actions"
|
||||
component={metadataOptionsContainer(connect)}
|
||||
|
Loading…
Reference in New Issue
Block a user