GT-721-change-schema-regsitry-tab-position

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/10335
GitOrigin-RevId: da2e2f53f85477b80e90dd8179ffde28108679f4
This commit is contained in:
Aaysha 2023-10-16 18:54:31 +05:30 committed by hasura-bot
parent 57b471ef03
commit f386414058
8 changed files with 245 additions and 224 deletions

View File

@ -3,6 +3,9 @@ import { Link, RouteComponentProps } from 'react-router';
import { isProConsole } from '../../../utils/proConsole';
import { useEELiteAccess } from '../../../features/EETrial';
import globals from '../../../Globals';
import { IconTooltip } from '../../../new-components/Tooltip';
import { FaRegMap } from 'react-icons/fa';
import { sendTelemetryEvent } from '../../../telemetry';
type TopNavProps = {
location: RouteComponentProps<unknown, unknown>['location'];
@ -25,6 +28,12 @@ const TopNav: React.FC<TopNavProps> = ({ location }) => {
dataTestVal: 'rest-explorer-link',
title: 'REST',
},
{
key: 'schema-registry',
link: '/api/schema-registry',
dataTestVal: 'schema-registry-link',
title: 'Schema Registry',
},
],
[
{
@ -68,13 +77,30 @@ const TopNav: React.FC<TopNavProps> = ({ location }) => {
: 'border-white hover:border-gray-100'
}`}
key={section.key}
onClick={() => {
// Send Telemetry data for Schema Registry tab
if (section.key === 'schema-registry') {
sendTelemetryEvent({
type: 'CLICK_EVENT',
data: {
id: 'schema-registry-top-nav-tab',
},
});
}
}}
>
<Link
to={section.link}
data-test={section.dataTestVal}
className="text-gray-600 font-semibold no-underline hover:text-gray-600 hover:no-underline focus:text-gray-600 focus:no-underline"
className="flex text-gray-600 font-semibold no-underline hover:text-gray-600 hover:no-underline focus:text-gray-600 focus:no-underline"
>
{section.title}
{section.key === 'schema-registry' && (
<IconTooltip
icon={<FaRegMap />}
message="Detect breaking and dangerous changes, view schema change history. Keep your GraphQL services safe and reliable! 🚀"
/>
)}
</Link>
</div>
))

View File

@ -15,7 +15,6 @@ import {
import { useEELiteAccess } from '../../../features/EETrial';
import { getQueryResponseCachingRoute } from '../../../utils/routeUtils';
import { isCloudConsole } from '../../../utils/cloudConsole';
import { isOpenTelemetrySupported } from '../../../utils/proConsole';
export interface Metadata {
@ -33,8 +32,7 @@ type SectionDataKey =
| 'security'
| 'monitoring'
| 'performance'
| 'about'
| 'graphql';
| 'about';
const Sidebar: React.FC<SidebarProps> = ({ location, metadata }) => {
const eeLiteAccess = useEELiteAccess(globals);
@ -47,25 +45,6 @@ const Sidebar: React.FC<SidebarProps> = ({ location, metadata }) => {
label: 'Metadata',
items: [],
};
if (
isCloudConsole(globals) &&
(globals.userRole === 'admin' || globals.userRole === 'owner')
) {
sectionsData.graphql = {
key: 'graphql',
label: 'GraphQL',
items: [
{
key: 'schema-registry',
label: 'Schema Registry (Beta)',
route: '/settings/schema-registry',
dataTestVal: 'metadata-schema-registry-link',
},
],
};
}
sectionsData.metadata.items.push({
key: 'actions',
label: 'Metadata Actions',

View File

@ -6,23 +6,19 @@ 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';
import { Analytics } from '../../Analytics';
import { Analytics, InitializeTelemetry } from '../../Analytics';
import { useGetV2Info } from '../hooks/useGetV2Info';
import { telemetryUserEventsTracker } from '../../../telemetry';
const SchemaRegistryHeader: React.VFC = () => {
const [isAlertModalOpen, setIsAlertModalOpen] = useState(false);
return (
<div className="flex flex-col w-full">
<div className="flex flex-col w-full pl-12 mb-2">
<div className="flex mb-xs mt-md w-full">
<h1 className="inline-block text-xl font-semibold mr-2 text-slate-900">
GraphQL Schema Registry
</h1>
<Badge className="mx-2" color="blue">
BETA
</Badge>
<Analytics name="data-schema-registry-alerts-btn">
<div
className="flex text-lg mt-2 mx-2 cursor-pointer"
@ -36,14 +32,6 @@ const SchemaRegistryHeader: React.VFC = () => {
</div>
</Analytics>
</div>
<a
className="text-muted w-auto mb-xs text-md"
href={SCHEMA_REGISTRY_REF_URL}
target="_blank"
rel="noreferrer noopener"
>
What is Schema Registry?
</a>
<span className="text-muted text-md mb-2 italic">
GraphQL Schema Registry changes will only be retained for 14 days.
</span>
@ -96,8 +84,9 @@ export const SchemaRegistryContainer: React.VFC<
);
return (
<div className="p-4 flex flex-col w-full">
<div className="flex flex-col w-[80%] pl-10 ml-10 justify-center">
<SchemaRegistryHeader />
<InitializeTelemetry tracker={telemetryUserEventsTracker} skip={false} />
<SchemaRegistryBody
hasFeatureAccess={hasFeatureAccess}
schemaId={schemaId}

View File

@ -4,42 +4,18 @@ import { useGetSchema } from '../hooks/useGetSchema';
import { Tabs } from '../../../new-components/Tabs';
import { IconTooltip } from '../../../new-components/Tooltip';
import { SchemaRow } from './SchemaRow';
import { ChangeSummary } from './ChangeSummary';
import {
findIfSubStringExists,
schemaTransformFn,
getPublishTime,
} from '../utils';
import { Link } from 'react-router';
import { RoleBasedSchema, Schema } from '../types';
import {
FaHome,
FaAngleRight,
FaFileImport,
FaSearch,
FaShareAlt,
} from 'react-icons/fa';
import { FaSearch, FaShareAlt } from 'react-icons/fa';
import { Input } from '../../../new-components/Form';
import AceEditor from 'react-ace';
import { SearchableSelect } from '../../../components/Common';
import { Analytics } from '../../Analytics';
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>
);
interface SchemaChangeDetailsProps {
schemaId: string;
}
@ -113,16 +89,17 @@ const SchemasDetails: React.VFC<{
/>
<div className="px-4 mb-2">
<div className="flex mt-4">
<div className="flex-col ">
<div className="flex items-center">
<p className="font-bold text-gray-500 py-2">Published</p>
<div className="flex items-center ">
<p className="font-bold text-gray-500 py-2">Published: </p>
<div className="flex items-center ml-2 font-semibold text-gray-600">
<span>{getPublishTime(schema.created_at)}</span>
<IconTooltip message="The time at which this GraphQL schema was generated" />
</div>
<span>{getPublishTime(schema.created_at)}</span>
</div>
<div className="flex items-center justify-around ml-auto w-1/2">
<div className="flex-col">
<div className="flex items-center">
<div className="flex items-center mt-4">
<p className="font-bold text-gray-500 py-2">Hash</p>
<IconTooltip message="Hash of the GraphQL Schema SDL. Hash for two identical schema is identical." />
</div>
@ -263,65 +240,64 @@ export const ChangesView: React.VFC<{
changesList.filter(c => c.criticality.level === 'NON_BREAKING');
const showBreakingChanges =
!selectedChangeLevel || selectedChangeLevel === 'breaking';
(!selectedChangeLevel || selectedChangeLevel === 'breaking') &&
!!breakingChanges?.length;
const showDangerousChanges =
!selectedChangeLevel || selectedChangeLevel === 'dangerous';
(!selectedChangeLevel || selectedChangeLevel === 'dangerous') &&
!!dangerousChanges?.length;
const showSafeChanges =
!selectedChangeLevel || selectedChangeLevel === 'safe';
(!selectedChangeLevel || selectedChangeLevel === 'safe') &&
!!safeChanges?.length;
const onFilterChange = (op: Option) => {
setSelectedChangeLevel(op.value);
};
return (
<div className="flex-col">
<div className="flex-col">
<div className="font-semibold text-lg mb-8 mt-8 text-gray-500">
Change Summary
</div>
<div className="mr-8">
<ChangeSummary changes={changes} />
</div>
</div>
<div className="flex w-full border-b border-gray-300 my-8" />
<div className="flex w-full mb-4 justify-between items-center">
<div className="flex w-full mt-4 mb-4 justify-between items-center">
<div className="flex font-semibold text-lg text-gray-500">Changes</div>
<div className="flex block w-[50%]">
<div className="flex w-[50%]">
<div className="flex w-full">
<div className="px-2 w-1/2">
<SearchableSelect
options={[
{
value: 'breaking',
label: 'Breaking',
},
{
value: 'dangerous',
label: 'Dangerous',
},
{
value: 'safe',
label: 'Safe',
},
]}
onChange={op => {
onFilterChange(op as Option);
}}
filterOption="prefix"
placeholder="Filter"
isClearable={true}
/>
</div>
<div className="pr-2 w-1/2">
<label>
<Input
type="text"
placeholder="Search"
name="search"
icon={<FaSearch />}
iconPosition="start"
onChange={handleSearch}
<Analytics name="schema-registry-schema-change-details-filter">
<SearchableSelect
options={[
{
value: 'breaking',
label: 'Breaking',
},
{
value: 'dangerous',
label: 'Dangerous',
},
{
value: 'safe',
label: 'Safe',
},
]}
onChange={op => {
onFilterChange(op as Option);
}}
filterOption="prefix"
placeholder="Filter"
isClearable={true}
/>
</label>
</Analytics>
</div>
<div className="pr-2 w-1/2">
<Analytics name="schema-registry-schema-change-details-search">
<label>
<Input
type="text"
placeholder="Search"
name="search"
icon={<FaSearch />}
iconPosition="start"
onChange={handleSearch}
/>
</label>
</Analytics>
</div>
</div>
</div>
@ -329,31 +305,27 @@ export const ChangesView: React.VFC<{
{showBreakingChanges && (
<div>
<div className="font-semibold text-md text-gray-500 mt-4">
<div className="flex items-center font-semibold text-md text-gray-500 mt-4">
Breaking
</div>
<div className="text-sm text-gray-600 pb-2">
Content for What is breaking on schema registry
<IconTooltip message="Breaking changes due the schema change" />
</div>
<ChangesTable changes={breakingChanges} level="breaking" />
</div>
)}
{showDangerousChanges && (
{showDangerousChanges && dangerousChanges && (
<div>
<div className="font-semibold text-md text-gray-500 mt-4">
<div className="flex items-center font-semibold text-md text-gray-500 mt-4">
Dangerous
</div>
<div className="text-sm text-gray-600 pb-2">
Content for What is dangerous on schema registry
<IconTooltip message="Dangerous changes due the schema change" />
</div>
<ChangesTable changes={dangerousChanges} level="dangerous" />
</div>
)}
{showSafeChanges && (
{showSafeChanges && safeChanges && (
<div>
<div className="font-semibold text-md text-gray-500 mt-4">Safe</div>
<div className="text-sm text-gray-600 pb-2">
Content for What is safe on schema registry
<div className="flex items-center font-semibold text-md text-gray-500 mt-4">
Safe
<IconTooltip message="Safe changes due the schema change" />
</div>
<ChangesTable changes={safeChanges} level="safe" />
</div>
@ -385,7 +357,7 @@ const ChangesTable: React.FC<ChangesTableProps> = ({ changes, level }) => {
</tr>
</thead>
<tbody>
{changes && changes.length ? (
{changes &&
changes.map((change, index) => (
<tr
className={`${textColor[level]} border border-neutral-200 p-2`}
@ -393,12 +365,7 @@ const ChangesTable: React.FC<ChangesTableProps> = ({ changes, level }) => {
>
<td className="pl-2">{change.message}</td>
</tr>
))
) : (
<tr>
<td className="pl-2">{`No ${level} changes`}</td>
</tr>
)}
))}
</tbody>
</table>
</div>

View File

@ -19,7 +19,7 @@ import AceEditor from 'react-ace';
export const Breadcrumbs = () => (
<div className="flex items-center space-x-xs mb-4">
<Link
to="/settings/schema-registry"
to="/api/schema-registry"
className="cursor-pointer flex items-center text-muted hover:text-gray-900"
>
<FaHome className="mr-1.5" />

View File

@ -78,7 +78,7 @@ export const SchemaRegistryHome: React.FC<SchemaRegistryHomeProps> = props => {
if (schemaRoleID === EMPTY_UUID_STRING) {
if (latestAdminRoleID) {
setSelectedRoleID(latestAdminRoleID);
dispatch(_push(`/settings/schema-registry/${latestAdminRoleID}`));
dispatch(_push(`/api/schema-registry/${latestAdminRoleID}`));
}
} else {
setSelectedRoleID(schemaRoleID);
@ -88,7 +88,7 @@ export const SchemaRegistryHome: React.FC<SchemaRegistryHomeProps> = props => {
useEffect(() => {
if (latestAdminRoleID) {
setSelectedRoleID(latestAdminRoleID);
dispatch(_push(`/settings/schema-registry/${latestAdminRoleID}`));
dispatch(_push(`/api/schema-registry/${latestAdminRoleID}`));
}
setSearchParam(pageNo);
}, [latestAdminRoleID, pageNo]);
@ -118,8 +118,8 @@ export const SchemaRegistryHome: React.FC<SchemaRegistryHomeProps> = props => {
fetchSchemasResponse.response
);
return (
<div className="flex w-full">
<div className="w-1/4">
<div className="flex w-[80%] justify-center">
<div className="w-[20%]">
<SchemaChangeList
schemas={schemaList}
urlSchemaCard={URLSchemaCard}
@ -172,27 +172,33 @@ export const SchemaChangeList: React.VFC<{
return (
<div className="overflow-x-auto rounded-md border-neutral-200 bg-white border mr-sm">
<div className="w-full flex bg-gray-100 px-4 py-2">
<div className="flex text-base w-[69%] justify-start">
<div className="flex text-base w-[50%] justify-start">
<span className="text-sm font-bold">PUBLISHED SCHEMA</span>
</div>
</div>
<div className="flex flex-col w-full">
{schemas.length ? (
<div className="mb-md">
{schemaList.map((schema, index) => (
<SchemaCard
cardKey={index}
openSchemaCardIndex={openSchemaCardIndex}
selectedRoleID={selectedRoleID}
createdAt={schema.created_at}
roles={schema.roles}
entryHash={schema.entry_hash}
tags={schema.tags}
dispatch={dispatch}
handleClick={() => setIsOpenSchemaCardIndex(index)}
/>
))}
<div className="flex w-full justify-center items-center">
<div
style={{ maxHeight: '80vh' }}
className={`flex flex-col w-full max-h-400px overflow-y-scroll`}
>
{schemaList.map((schema, index) => (
<SchemaCard
cardKey={index}
openSchemaCardIndex={openSchemaCardIndex}
selectedRoleID={selectedRoleID}
createdAt={schema.created_at}
roles={schema.roles}
entryHash={schema.entry_hash}
tags={schema.tags}
dispatch={dispatch}
handleClick={() => setIsOpenSchemaCardIndex(index)}
/>
))}
</div>
<div className="flex w-full justify-center items-center mt-2">
<button
className="btn btn-primary items-center max-w-full justify-center inline-flex text-sm font-sans font-semibold border rounded shadow-sm focus-visible:outline-none h-btn px-sm mx-1 disabled:border-gray-300 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={pageNumber === 0}
@ -281,7 +287,8 @@ const SchemaCard: React.VFC<{
handleClick,
} = props;
const [isTagModalOpen, setIsTagModalOpen] = React.useState(false);
const [isRolesMenuOpen, setIsRolesMenuOpen] = useState(false);
const [isCurrentCardOpen, setIsCurrentCardOpen] = useState(false);
const [tagsList, setTagsList] = React.useState<SchemaRegistryTag[]>(tags);
const onRemoveTag = (id: string) => {
const filteredTags = tagsList.filter(
@ -289,92 +296,128 @@ const SchemaCard: React.VFC<{
);
setTagsList(filteredTags);
};
const adminIndex = roles.findIndex(role => role.role === 'admin');
// If "admin" is not at the 0 index, move it there
if (adminIndex !== 0 && adminIndex !== -1) {
const adminRole = roles.splice(adminIndex, 1)[0];
roles.unshift(adminRole);
}
useEffect(() => {
setTagsList(tags);
}, [tags]);
React.useEffect(() => {
setIsRolesMenuOpen(cardKey === openSchemaCardIndex);
setIsCurrentCardOpen(cardKey === openSchemaCardIndex);
}, [cardKey, openSchemaCardIndex]);
const handleRoleClick = (roleBasedChange: Role) => {
dispatch(_push(`/settings/schema-registry/${roleBasedChange.id}`));
console.log('click');
dispatch(_push(`/api/schema-registry/${roleBasedChange.id}`));
};
const defaultShowAllRoles = isCurrentCardOpen || roles.length <= 3;
const RolesList = defaultShowAllRoles ? roles : roles.slice(0, 3);
return (
<div
className="w-full flex flex-col px-4 bg-white rounded"
onClick={handleClick}
className={`w-full flex flex-col px-4 bg-white rounded mb-1`}
onClick={() => {
if (!roles.some(role => role.id === selectedRoleID)) {
dispatch(_push(`/api/schema-registry/${roles[0].id}`));
}
handleClick();
}}
>
<div className="flex flex-col mt-2 justify-between h-full">
<div className="flex text-gray-600 text-sm justify-start">
<div
className={`mt-2 ${
isCurrentCardOpen ? 'bg-gray-100' : ' bg-white'
} hover:bg-gray-100 `}
>
<div className="flex mt-1 mb-2 text-gray-600 text-sm items-center justify-start h-full px-2">
<span>{getPublishTime(createdAt)}</span>
<span>
<Analytics name="schema-registry-add-tag-btn">
{isTagModalOpen && (
<AddSchemaRegistryTagDialog
tagsList={tagsList}
setTagsList={setTagsList}
entryHash={entryHash}
onClose={() => {
setIsTagModalOpen(false);
}}
/>
)}
<div className="flex items-center">
<div className="flex flex-nowrap items-center justify-start ">
{tagsList &&
tagsList.map(schemaRegistryTag => (
<div className="mx-1 ">
<SchemaTag
schemaRegistryTag={schemaRegistryTag}
onRemove={onRemoveTag}
/>
</div>
))}
<div className="ml-1" onClick={() => setIsTagModalOpen(true)}>
<span>
<IconTooltip
message="Add a Tag"
icon={<FaPlusCircle />}
/>
</span>
</div>
</div>
</div>
</Analytics>
</span>
</div>
<div
className={`flex font-semibold text-md justify-end hover:text-gray-600 ${
isRolesMenuOpen ? 'text-gray-600' : 'text-gray-400'
} cursor-pointer`}
className={`flex flex-col justify-start w-full pt-auto ${
isCurrentCardOpen ? 'bg-gray-100' : ''
} rounded `}
>
{roles.length} {roles.length === 1 ? 'Role' : 'Roles'}
<span className="mr-1">{<FaChevronDown />}</span>
</div>
</div>
{isRolesMenuOpen && (
<div className="w-full pt-auto">
<div className="text-sm text-gray-500 font-bold mb-3">
{roles.length === 1 ? 'ROLE' : 'ROLES'}
<div className="text-sm text-gray-500 font-bold mb-1 px-2">
{roles.length} {roles.length === 1 ? 'ROLE' : 'ROLES'}
</div>
{roles.map((roleBasedChange, index) => (
<div
className={`flex w-full p-2 ${
roleBasedChange.id === selectedRoleID ? 'bg-gray-100' : ''
} rounded hover:bg-gray-200`}
onClick={() => {
handleRoleClick(roleBasedChange);
}}
key={index}
>
<div className="flex items-center justify-between w-full rounded">
<div className="text-base rounded cursor-pointer">
<p className="text-sm text-teal-800 font-bold bg-gray-200 px-1 rounded">
{CapitalizeFirstLetter(roleBasedChange.role)}
</p>
<div>
{RolesList.map((roleBasedChange, index) => (
<div
className={`flex w-full px-2 py-1 ${
isCurrentCardOpen && roleBasedChange.id === selectedRoleID
? 'bg-gray-200'
: ''
} rounded hover:bg-gray-200`}
onClick={() => {
handleRoleClick(roleBasedChange);
}}
key={index}
>
<div className="flex items-center justify-between w-full rounded">
<div className="text-base rounded cursor-pointer">
<p className="text-sm text-teal-800 font-bold bg-gray-200 px-1 rounded">
{CapitalizeFirstLetter(roleBasedChange.role)}
</p>
</div>
<FaChevronRight />
</div>
<FaChevronRight />
</div>
</div>
))}
</div>
)}
{isTagModalOpen && (
<AddSchemaRegistryTagDialog
tagsList={tagsList}
setTagsList={setTagsList}
entryHash={entryHash}
onClose={() => {
setIsTagModalOpen(false);
}}
/>
)}
<div className="flex flex-nowrap items-center justify-start mt-2">
{tagsList &&
tagsList.map(schemaRegistryTag => (
<div className="mr-2 ">
<SchemaTag
schemaRegistryTag={schemaRegistryTag}
onRemove={onRemoveTag}
/>
</div>
))}
<Analytics name="schema-registry-add-tag-btn">
<div
className="mt-[7px] ml-[-6px] pb-2"
onClick={() => setIsTagModalOpen(true)}
>
<span>
<IconTooltip message="Add a Tag" icon={<FaPlusCircle />} />
</span>
))}
{!defaultShowAllRoles && (
<Analytics name="schema-registry-see-more-roles-btn">
<div
className="flex justify-center items-center cursor-pointer text-base hover:bg-gray-200 pt-1"
//here there is no need for onClick as clicking anywhere on a schemaCard sets the schemaCard as open and defaultShowAllRoles will be set to true
>
<span className="text-gray font-semibold">
See More Roles
</span>
<FaChevronDown />
</div>
</Analytics>
)}
</div>
</Analytics>
</div>
</div>
<div className="flex w-full border-b border-gray-300 my-2"></div>
<div className="flex w-full border-b border-gray-300 my-1"></div>
</div>
);
};

View File

@ -3,6 +3,7 @@ import React from 'react';
import { Link, RouteComponentProps } from 'react-router';
import { FaCheckCircle, FaTimesCircle } from 'react-icons/fa';
import { LocationDescriptor } from 'history';
import { sendTelemetryEvent } from '../../telemetry';
export type NavigationSidebarItem = {
key: string;
@ -50,6 +51,17 @@ export const NavigationSidebar = ({
? 'text-amber-500'
: 'text-muted'
)}
onClick={() => {
// Check if section.key is "schema-registry" and trigger telemetry event
if (item.key === 'schema-registry') {
sendTelemetryEvent({
type: 'CLICK_EVENT',
data: {
id: 'schema-registry-settings-btn',
},
});
}
}}
>
{item.label}
</div>

View File

@ -346,6 +346,11 @@ const routes = store => {
<Route path="details/:name" component={RestEndpointDetailsPage} />
<Route path="edit/:name" component={CreateRestView} />
</Route>
<Route path="schema-registry" component={SchemaRegistryContainer} />
<Route
path="schema-registry/:id"
component={SchemaRegistryContainer}
/>
<Route path="allow-list">
<IndexRedirect to="detail" />
<Route