mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
feat: native composition report (#7007)
This commit is contained in:
parent
cefd6f24db
commit
b65576f8a0
10 changed files with 294 additions and 103 deletions
|
|
@ -34,8 +34,8 @@ const config: CodegenConfig = {
|
||||||
contextType: 'GraphQLModules.ModuleContext',
|
contextType: 'GraphQLModules.ModuleContext',
|
||||||
enumValues: {
|
enumValues: {
|
||||||
ProjectType: '../shared/entities#ProjectType',
|
ProjectType: '../shared/entities#ProjectType',
|
||||||
NativeFederationCompatibilityStatus:
|
NativeFederationCompatibilityStatusType:
|
||||||
'../shared/entities#NativeFederationCompatibilityStatus',
|
'../shared/entities#NativeFederationCompatibilityStatusType',
|
||||||
TargetAccessScope: '../modules/auth/providers/scopes#TargetAccessScope',
|
TargetAccessScope: '../modules/auth/providers/scopes#TargetAccessScope',
|
||||||
ProjectAccessScope: '../modules/auth/providers/scopes#ProjectAccessScope',
|
ProjectAccessScope: '../modules/auth/providers/scopes#ProjectAccessScope',
|
||||||
OrganizationAccessScope: '../modules/auth/providers/scopes#OrganizationAccessScope',
|
OrganizationAccessScope: '../modules/auth/providers/scopes#OrganizationAccessScope',
|
||||||
|
|
|
||||||
|
|
@ -281,12 +281,13 @@ function isActionMatch(actionContainingWildcard: string, action: string) {
|
||||||
if (actionContainingWildcard === '*') {
|
if (actionContainingWildcard === '*') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// exact match
|
// exact match
|
||||||
if (actionContainingWildcard === action) {
|
if (actionContainingWildcard === action) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [actionScope] = action.split(':');
|
const [actionScope, actionId] = action.split(':');
|
||||||
const [userSpecifiedActionScope, userSpecifiedActionId] = actionContainingWildcard.split(':');
|
const [userSpecifiedActionScope, userSpecifiedActionId] = actionContainingWildcard.split(':');
|
||||||
|
|
||||||
// wildcard match "scope:*"
|
// wildcard match "scope:*"
|
||||||
|
|
@ -294,6 +295,11 @@ function isActionMatch(actionContainingWildcard: string, action: string) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wildcard match "*:scope"
|
||||||
|
if (userSpecifiedActionScope === '*' && userSpecifiedActionId === actionId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,7 +506,7 @@ type ActionDefinitionMap = {
|
||||||
[key: `${string}:${string}`]: (args: any) => Array<string>;
|
[key: `${string}:${string}`]: (args: any) => Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionDefinitions = {
|
export const actionDefinitions = {
|
||||||
...objectFromEntries(permissionsByLevel['organization'].map(t => [t.value, defaultOrgIdentity])),
|
...objectFromEntries(permissionsByLevel['organization'].map(t => [t.value, defaultOrgIdentity])),
|
||||||
...objectFromEntries(permissionsByLevel['project'].map(t => [t.value, defaultProjectIdentity])),
|
...objectFromEntries(permissionsByLevel['project'].map(t => [t.value, defaultProjectIdentity])),
|
||||||
...objectFromEntries(permissionsByLevel['target'].map(t => [t.value, defaultTargetIdentity])),
|
...objectFromEntries(permissionsByLevel['target'].map(t => [t.value, defaultTargetIdentity])),
|
||||||
|
|
@ -514,7 +520,7 @@ const actionDefinitions = {
|
||||||
|
|
||||||
type Actions = keyof typeof actionDefinitions;
|
type Actions = keyof typeof actionDefinitions;
|
||||||
|
|
||||||
type ActionStrings = Actions | '*';
|
type ActionStrings = Actions | '*' | '*:describe';
|
||||||
|
|
||||||
/** Unauthenticated session that is returned by default. */
|
/** Unauthenticated session that is returned by default. */
|
||||||
class UnauthenticatedSession extends Session {
|
class UnauthenticatedSession extends Session {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,18 @@ export class SuperTokensCookieBasedSession extends Session {
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Allow admins to use all describe actions within foreign organizations
|
||||||
|
// This makes it much more pleasant to debug.
|
||||||
|
if (user.isAdmin) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action: '*:describe',
|
||||||
|
effect: 'allow',
|
||||||
|
resource: `hrn:${organizationId}:organization/${organizationId}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ export class ProjectManager {
|
||||||
projectId: selector.projectId,
|
projectId: selector.projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.storage.getProject(selector);
|
return this.storage.getProject(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,14 +114,40 @@ export default gql`
|
||||||
externalSchemaComposition: ExternalSchemaComposition
|
externalSchemaComposition: ExternalSchemaComposition
|
||||||
schemaVersionsCount(period: DateRangeInput): Int!
|
schemaVersionsCount(period: DateRangeInput): Int!
|
||||||
isNativeFederationEnabled: Boolean!
|
isNativeFederationEnabled: Boolean!
|
||||||
nativeFederationCompatibility: NativeFederationCompatibilityStatus!
|
"""
|
||||||
|
Get the status of the native federation compatability for the project.
|
||||||
|
"""
|
||||||
|
nativeFederationCompatibility: NativeCompositionCompatibility!
|
||||||
}
|
}
|
||||||
|
|
||||||
extend type Target {
|
extend type Target {
|
||||||
schemaVersionsCount(period: DateRangeInput): Int!
|
schemaVersionsCount(period: DateRangeInput): Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NativeFederationCompatibilityStatus {
|
type NativeCompositionVersionStatus {
|
||||||
|
"""
|
||||||
|
The schema version we check against.
|
||||||
|
"""
|
||||||
|
schemaVersion: SchemaVersion!
|
||||||
|
"""
|
||||||
|
The native composition result. The supergraphSdl is sorted and normalized.
|
||||||
|
"""
|
||||||
|
nativeCompositionResult: SchemaCompositionResult!
|
||||||
|
"""
|
||||||
|
The supergraph of the latest valid schema version (sorted and normalized).
|
||||||
|
"""
|
||||||
|
currentSupergraphSdl: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type NativeCompositionCompatibility {
|
||||||
|
"""
|
||||||
|
Whether the schema version is compatible.
|
||||||
|
"""
|
||||||
|
status: NativeFederationCompatibilityStatusType!
|
||||||
|
results: [NativeCompositionVersionStatus]!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NativeFederationCompatibilityStatusType {
|
||||||
COMPATIBLE
|
COMPATIBLE
|
||||||
INCOMPATIBLE
|
INCOMPATIBLE
|
||||||
UNKNOWN
|
UNKNOWN
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { SchemaChecksFilter } from '../../../__generated__/types';
|
||||||
import * as GraphQLSchema from '../../../__generated__/types';
|
import * as GraphQLSchema from '../../../__generated__/types';
|
||||||
import {
|
import {
|
||||||
DateRange,
|
DateRange,
|
||||||
NativeFederationCompatibilityStatus,
|
NativeFederationCompatibilityStatusType,
|
||||||
Organization,
|
Organization,
|
||||||
Project,
|
Project,
|
||||||
ProjectType,
|
ProjectType,
|
||||||
|
|
@ -1123,7 +1123,18 @@ export class SchemaManager {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNativeFederationCompatibilityStatus(project: Project) {
|
async getNativeFederationCompatibilityStatus(project: Project): Promise<{
|
||||||
|
status: NativeFederationCompatibilityStatusType;
|
||||||
|
results: Array<null | {
|
||||||
|
schemaVersion: SchemaVersion;
|
||||||
|
target: Target;
|
||||||
|
nativeCompositionResult: {
|
||||||
|
supergraphSdl: string | null;
|
||||||
|
errors: Array<{ message: string }> | null;
|
||||||
|
};
|
||||||
|
currentSupergraphSdl: string;
|
||||||
|
}>;
|
||||||
|
}> {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Get native Federation compatibility status (organization=%s, project=%s)',
|
'Get native Federation compatibility status (organization=%s, project=%s)',
|
||||||
project.orgId,
|
project.orgId,
|
||||||
|
|
@ -1131,7 +1142,10 @@ export class SchemaManager {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (project.type !== ProjectType.FEDERATION) {
|
if (project.type !== ProjectType.FEDERATION) {
|
||||||
return NativeFederationCompatibilityStatus.NOT_APPLICABLE;
|
return {
|
||||||
|
status: NativeFederationCompatibilityStatusType.NOT_APPLICABLE,
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const targets = await this.targetManager.getTargets({
|
const targets = await this.targetManager.getTargets({
|
||||||
|
|
@ -1139,48 +1153,39 @@ export class SchemaManager {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const possibleVersions = await Promise.all(
|
const results = await Promise.all(
|
||||||
targets.map(target => this.getMaybeLatestValidVersion(target)),
|
targets.map(async target => {
|
||||||
);
|
const schemaVersion = await this.getMaybeLatestValidVersion(target);
|
||||||
|
|
||||||
const versions = possibleVersions.filter((v): v is SchemaVersion => !!v);
|
if (schemaVersion === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.debug('Found %s targets and %s versions', targets.length, versions.length);
|
const currentSupergraphSdl = print(
|
||||||
|
removeDescriptions(
|
||||||
|
sortSDL(
|
||||||
|
parseGraphQLSource(
|
||||||
|
schemaVersion.supergraphSDL!,
|
||||||
|
'parsing native supergraph in getNativeFederationCompatibilityStatus',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// If there are no composable versions available, we can't determine the compatibility status.
|
const schemas = await this.getSchemasOfVersion({
|
||||||
if (
|
organizationId: target.orgId,
|
||||||
versions.length === 0 ||
|
projectId: target.projectId,
|
||||||
!versions.every(
|
targetId: target.id,
|
||||||
version => version && version.isComposable && typeof version.supergraphSDL === 'string',
|
versionId: schemaVersion.id,
|
||||||
)
|
});
|
||||||
) {
|
|
||||||
this.logger.debug('No composable versions available (status: unknown)');
|
|
||||||
return NativeFederationCompatibilityStatus.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemasPerVersion = await Promise.all(
|
if (schemas.length === 0) {
|
||||||
versions.map(async version =>
|
return null;
|
||||||
this.getSchemasOfVersion({
|
|
||||||
organizationId: version.organizationId,
|
|
||||||
projectId: version.projectId,
|
|
||||||
targetId: version.targetId,
|
|
||||||
versionId: version.id,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.debug('Checking compatibility of %s versions', versions.length);
|
|
||||||
|
|
||||||
const compatibilityResults = await Promise.all(
|
|
||||||
versions.map(async (version, i) => {
|
|
||||||
if (schemasPerVersion[i].length === 0) {
|
|
||||||
this.logger.debug('No schemas (version=%s)', version.id);
|
|
||||||
return NativeFederationCompatibilityStatus.UNKNOWN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const compositionResult = await this.compositionOrchestrator.composeAndValidate(
|
const compositionResult = await this.compositionOrchestrator.composeAndValidate(
|
||||||
'federation',
|
'federation',
|
||||||
ensureCompositeSchemas(schemasPerVersion[i]).map(s =>
|
ensureCompositeSchemas(schemas).map(s =>
|
||||||
this.schemaHelper.createSchemaObject({
|
this.schemaHelper.createSchemaObject({
|
||||||
sdl: s.sdl,
|
sdl: s.sdl,
|
||||||
service_name: s.service_name,
|
service_name: s.service_name,
|
||||||
|
|
@ -1196,53 +1201,52 @@ export class SchemaManager {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (compositionResult.supergraph) {
|
const supergraphSdl = compositionResult.supergraph
|
||||||
const sortedExistingSupergraph = print(
|
? print(
|
||||||
removeDescriptions(
|
removeDescriptions(
|
||||||
sortSDL(
|
sortSDL(
|
||||||
parseGraphQLSource(
|
parseGraphQLSource(
|
||||||
compositionResult.supergraph,
|
compositionResult.supergraph,
|
||||||
'parsing existing supergraph in getNativeFederationCompatibilityStatus',
|
'parsing native supergraph in getNativeFederationCompatibilityStatus',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
);
|
: null;
|
||||||
const sortedNativeSupergraph = print(
|
|
||||||
removeDescriptions(
|
|
||||||
sortSDL(
|
|
||||||
parseGraphQLSource(
|
|
||||||
version.supergraphSDL!,
|
|
||||||
'parsing native supergraph in getNativeFederationCompatibilityStatus',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sortedNativeSupergraph === sortedExistingSupergraph) {
|
return {
|
||||||
return NativeFederationCompatibilityStatus.COMPATIBLE;
|
target,
|
||||||
}
|
schemaVersion,
|
||||||
|
currentSupergraphSdl,
|
||||||
this.logger.debug('Produced different supergraph (version=%s)', version.id);
|
nativeCompositionResult: {
|
||||||
} else {
|
supergraphSdl,
|
||||||
this.logger.debug('Failed to produce supergraph (version=%s)', version.id);
|
errors: compositionResult.errors,
|
||||||
}
|
},
|
||||||
|
};
|
||||||
return NativeFederationCompatibilityStatus.INCOMPATIBLE;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (compatibilityResults.includes(NativeFederationCompatibilityStatus.UNKNOWN)) {
|
let status = NativeFederationCompatibilityStatusType.INCOMPATIBLE;
|
||||||
this.logger.debug('One of the versions seems empty (status: unknown)');
|
|
||||||
return NativeFederationCompatibilityStatus.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (compatibilityResults.every(r => r === NativeFederationCompatibilityStatus.COMPATIBLE)) {
|
if (results.every(result => result === null)) {
|
||||||
|
this.logger.debug('No composable versions available (status: unknown)');
|
||||||
|
status = NativeFederationCompatibilityStatusType.UNKNOWN;
|
||||||
|
} else if (
|
||||||
|
results.every(
|
||||||
|
result =>
|
||||||
|
result === null ||
|
||||||
|
(result.nativeCompositionResult &&
|
||||||
|
result.currentSupergraphSdl === result.nativeCompositionResult.supergraphSdl),
|
||||||
|
)
|
||||||
|
) {
|
||||||
this.logger.debug('All versions are compatible (status: compatible)');
|
this.logger.debug('All versions are compatible (status: compatible)');
|
||||||
return NativeFederationCompatibilityStatus.COMPATIBLE;
|
status = NativeFederationCompatibilityStatusType.COMPATIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Some versions are incompatible (status: incompatible)');
|
return {
|
||||||
return NativeFederationCompatibilityStatus.INCOMPATIBLE;
|
status,
|
||||||
|
results,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGitHubMetadata(schemaVersion: SchemaVersion): Promise<null | {
|
async getGitHubMetadata(schemaVersion: SchemaVersion): Promise<null | {
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ export enum ProjectType {
|
||||||
SINGLE = 'SINGLE',
|
SINGLE = 'SINGLE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NativeFederationCompatibilityStatus {
|
export enum NativeFederationCompatibilityStatusType {
|
||||||
COMPATIBLE = 'COMPATIBLE',
|
COMPATIBLE = 'COMPATIBLE',
|
||||||
INCOMPATIBLE = 'INCOMPATIBLE',
|
INCOMPATIBLE = 'INCOMPATIBLE',
|
||||||
UNKNOWN = 'UNKNOWN',
|
UNKNOWN = 'UNKNOWN',
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { Switch } from '@/components/ui/switch';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||||
import { NativeFederationCompatibilityStatus } from '@/gql/graphql';
|
import { NativeFederationCompatibilityStatusType } from '@/gql/graphql';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const IncrementalNativeCompositionSwitch_TargetFragment = graphql(`
|
const IncrementalNativeCompositionSwitch_TargetFragment = graphql(`
|
||||||
|
|
@ -121,7 +121,9 @@ const NativeCompositionSettings_ProjectQuery = graphql(`
|
||||||
query NativeCompositionSettings_ProjectQuery($selector: ProjectSelectorInput!) {
|
query NativeCompositionSettings_ProjectQuery($selector: ProjectSelectorInput!) {
|
||||||
project(reference: { bySelector: $selector }) {
|
project(reference: { bySelector: $selector }) {
|
||||||
id
|
id
|
||||||
nativeFederationCompatibility
|
nativeFederationCompatibility {
|
||||||
|
status
|
||||||
|
}
|
||||||
experimental_nativeCompositionPerTarget
|
experimental_nativeCompositionPerTarget
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -290,37 +292,37 @@ export function NativeCompositionSettings(props: {
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-row items-center gap-x-4">
|
<div className="flex flex-row items-center gap-x-4">
|
||||||
<div>
|
<div>
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Compatible ? (
|
NativeFederationCompatibilityStatusType.Compatible ? (
|
||||||
<PartyPopperIcon className="size-10 text-emerald-500" />
|
<PartyPopperIcon className="size-10 text-emerald-500" />
|
||||||
) : null}
|
) : null}
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Incompatible ? (
|
NativeFederationCompatibilityStatusType.Incompatible ? (
|
||||||
<HeartCrackIcon className="size-10 text-red-500" />
|
<HeartCrackIcon className="size-10 text-red-500" />
|
||||||
) : null}
|
) : null}
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Unknown ? (
|
NativeFederationCompatibilityStatusType.Unknown ? (
|
||||||
<FlaskConicalIcon className="size-10 text-orange-500" />
|
<FlaskConicalIcon className="size-10 text-orange-500" />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-semibold">
|
<div className="text-base font-semibold">
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Compatible
|
NativeFederationCompatibilityStatusType.Compatible
|
||||||
? 'Your project is compatible'
|
? 'Your project is compatible'
|
||||||
: null}
|
: null}
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Incompatible
|
NativeFederationCompatibilityStatusType.Incompatible
|
||||||
? 'Your project is not yet supported'
|
? 'Your project is not yet supported'
|
||||||
: null}
|
: null}
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Unknown
|
NativeFederationCompatibilityStatusType.Unknown
|
||||||
? 'Unclear whether your project is compatible'
|
? 'Unclear whether your project is compatible'
|
||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground text-sm">
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Compatible ? (
|
NativeFederationCompatibilityStatusType.Compatible ? (
|
||||||
<>
|
<>
|
||||||
Subgraphs of this project are composed and validated correctly by our{' '}
|
Subgraphs of this project are composed and validated correctly by our{' '}
|
||||||
<a
|
<a
|
||||||
|
|
@ -332,8 +334,8 @@ export function NativeCompositionSettings(props: {
|
||||||
for Apollo Federation.
|
for Apollo Federation.
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Incompatible ? (
|
NativeFederationCompatibilityStatusType.Incompatible ? (
|
||||||
<>
|
<>
|
||||||
Our{' '}
|
Our{' '}
|
||||||
<a
|
<a
|
||||||
|
|
@ -344,11 +346,17 @@ export function NativeCompositionSettings(props: {
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
is not yet compatible with subgraphs of your project. We're working on it!
|
is not yet compatible with subgraphs of your project. We're working on it!
|
||||||
<br />
|
<br />
|
||||||
Please reach out to us to explore solutions for addressing this issue.
|
Please reach out to us to explore solutions for addressing this issue and share
|
||||||
|
this report with us:
|
||||||
|
<a
|
||||||
|
href={`/native-composition-compatibility-report/${projectQuery.data.project.id}`}
|
||||||
|
>
|
||||||
|
View full report
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{projectQuery.data.project.nativeFederationCompatibility ===
|
{projectQuery.data.project.nativeFederationCompatibility.status ===
|
||||||
NativeFederationCompatibilityStatus.Unknown ? (
|
NativeFederationCompatibilityStatusType.Unknown ? (
|
||||||
<>
|
<>
|
||||||
Your project appears to lack any subgraphs at the moment, making it impossible
|
Your project appears to lack any subgraphs at the moment, making it impossible
|
||||||
for us to assess compatibility with our{' '}
|
for us to assess compatibility with our{' '}
|
||||||
|
|
|
||||||
123
packages/web/app/src/pages/native-composition-diff.tsx
Normal file
123
packages/web/app/src/pages/native-composition-diff.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { useQuery } from 'urql';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { DiffEditor } from '@/components/v2';
|
||||||
|
import { graphql } from '@/gql';
|
||||||
|
import { useClipboard } from '@/lib/hooks';
|
||||||
|
|
||||||
|
type NativeCompositionDiffProps = {
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NativeCompositionDiff_NativeCompositionDiffQuery = graphql(/* GraphQL */ `
|
||||||
|
query NativeCompositionDiff_NativeCompositionDiffQuery($projectId: ID!) {
|
||||||
|
project(reference: { byId: $projectId }) {
|
||||||
|
id
|
||||||
|
nativeFederationCompatibility {
|
||||||
|
status
|
||||||
|
results {
|
||||||
|
currentSupergraphSdl
|
||||||
|
nativeCompositionResult {
|
||||||
|
supergraphSdl
|
||||||
|
errors {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schemaVersion {
|
||||||
|
id
|
||||||
|
schemas {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
... on CompositeSchema {
|
||||||
|
id
|
||||||
|
service
|
||||||
|
source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export function NativeCompositionDiff(props: NativeCompositionDiffProps): ReactNode | null {
|
||||||
|
const [result] = useQuery({
|
||||||
|
query: NativeCompositionDiff_NativeCompositionDiffQuery,
|
||||||
|
variables: {
|
||||||
|
projectId: props.projectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
|
if (!result.data?.project?.nativeFederationCompatibility) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { nativeFederationCompatibility } = result.data.project;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Native Composition Report Project {result.data.project.id}</h1>
|
||||||
|
<h2>Status: {nativeFederationCompatibility.status}</h2>
|
||||||
|
<Tabs>
|
||||||
|
<TabsList>
|
||||||
|
{nativeFederationCompatibility.results.map((result, index) =>
|
||||||
|
result ? <TabsTrigger value={String(index)}>Target {index}</TabsTrigger> : null,
|
||||||
|
)}
|
||||||
|
</TabsList>
|
||||||
|
{nativeFederationCompatibility.results.map((result, index) =>
|
||||||
|
result ? (
|
||||||
|
<TabsContent value={String(index)}>
|
||||||
|
<div>
|
||||||
|
<div>Supergraph Diff</div>
|
||||||
|
<div>
|
||||||
|
<DiffEditor
|
||||||
|
before={result.currentSupergraphSdl}
|
||||||
|
after={result.nativeCompositionResult.supergraphSdl ?? null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Composition Errors:</div>
|
||||||
|
<div>
|
||||||
|
{result.nativeCompositionResult.errors?.edges?.length ? (
|
||||||
|
<ul>
|
||||||
|
{result.nativeCompositionResult.errors.edges.map(edge => (
|
||||||
|
<li>{edge.node.message}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
'None'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-10">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const services = result.schemaVersion?.schemas.edges.map(edge => ({
|
||||||
|
sdl: (edge.node as any).source,
|
||||||
|
name: (edge.node as any).service,
|
||||||
|
}));
|
||||||
|
|
||||||
|
void clipboard(JSON.stringify(services, null, 2));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy services to clipboard
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ import { DevPage } from './pages/dev';
|
||||||
import { IndexPage } from './pages/index';
|
import { IndexPage } from './pages/index';
|
||||||
import { LogoutPage } from './pages/logout';
|
import { LogoutPage } from './pages/logout';
|
||||||
import { ManagePage } from './pages/manage';
|
import { ManagePage } from './pages/manage';
|
||||||
|
import { NativeCompositionDiff } from './pages/native-composition-diff';
|
||||||
import { OrganizationIndexRouteSearch, OrganizationPage } from './pages/organization';
|
import { OrganizationIndexRouteSearch, OrganizationPage } from './pages/organization';
|
||||||
import { JoinOrganizationPage } from './pages/organization-join';
|
import { JoinOrganizationPage } from './pages/organization-join';
|
||||||
import { OrganizationMembersPage } from './pages/organization-members';
|
import { OrganizationMembersPage } from './pages/organization-members';
|
||||||
|
|
@ -301,6 +302,15 @@ const devRoute = createRoute({
|
||||||
component: DevPage,
|
component: DevPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nativeCompositionDiffRoute = createRoute({
|
||||||
|
getParentRoute: () => authenticatedRoute,
|
||||||
|
path: 'native-composition-compatibility-report/$projectId',
|
||||||
|
component: function NativeCompositionDiffRoute() {
|
||||||
|
const { projectId } = nativeCompositionDiffRoute.useParams();
|
||||||
|
return <NativeCompositionDiff projectId={projectId} />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const newOrgPage = createRoute({
|
const newOrgPage = createRoute({
|
||||||
getParentRoute: () => authenticatedRoute,
|
getParentRoute: () => authenticatedRoute,
|
||||||
path: 'org/new',
|
path: 'org/new',
|
||||||
|
|
@ -836,6 +846,7 @@ const routeTree = root.addChildren([
|
||||||
]),
|
]),
|
||||||
authenticatedRoute.addChildren([
|
authenticatedRoute.addChildren([
|
||||||
indexRoute,
|
indexRoute,
|
||||||
|
nativeCompositionDiffRoute,
|
||||||
devRoute,
|
devRoute,
|
||||||
newOrgPage,
|
newOrgPage,
|
||||||
manageRoute,
|
manageRoute,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue