diff --git a/.changeset/bright-crabs-crash.md b/.changeset/bright-crabs-crash.md new file mode 100644 index 0000000000..f5518505b5 --- /dev/null +++ b/.changeset/bright-crabs-crash.md @@ -0,0 +1,39 @@ +--- +'app': major +--- + +Use dynamic frontend plugins across the app + +This change makes `dynamicPlugins.frontend.mountPoints` generic and declarative: + +Mountpoint now support following names/types: + +- Allow passing \*/context mountpoints for React context +- Allow passing \*/cards for Card components (with layout) + +- `entity.page.overview` +- `entity.page.topology` +- `entity.page.issues` +- `entity.page.pull-requests` +- `entity.page.ci` +- `entity.page.cd` +- `entity.page.kubernetes` +- `entity.page.tekton` +- `entity.page.image-registry` +- `entity.page.monitoring` +- `entity.page.lighthouse` +- `entity.page.api` +- `entity.page.dependencies` +- `entity.page.docs` +- `entity.page.definition` +- `entity.page.diagram` + +Mountpoints support following configuration: + +- `layout` for layout features that propagates to allowing users to use CSS properties gridColumnStart including responsiveness queries etc. (mui.com/system/getting-started/the-sx-prop) +- `if` for EntitySwitch.Case if=... - allows allOf|anyOf|oneOf conditionals with isKind|isType|hasAnnotation builtin methods or code imports via Scalprum (direct string reference) +- `props` to pass additional props to the mounted component + +Current limitations of the dynamic frontend plugins: + +Allows you to mount to existing mountPoints only. You're unable to create additional tabs for example. (will be addressed in a follow up PR) diff --git a/app-config.yaml b/app-config.yaml index cc70dcfd5b..b769aec930 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -342,4 +342,334 @@ enabled: dynamicPlugins: rootDirectory: dynamic-plugins-root - frontend: {} + frontend: + backstage.plugin-azure-devops: + mountPoints: + - mountPoint: entity.page.ci/cards + module: AzureDevopsPlugin + importName: EntityAzurePipelinesContent + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isAzureDevOpsAvailable + - mountPoint: entity.page.pull-requests/cards + module: AzureDevopsPlugin + importName: EntityAzurePullRequestsContent + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isAzureDevOpsAvailable + backstage.plugin-dynatrace: + mountPoints: + - mountPoint: entity.page.monitoring/cards + module: DynatracePlugin + importName: DynatraceTab + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isDynatraceAvailable + backstage.plugin-github-actions: + mountPoints: + - mountPoint: entity.page.ci/cards + module: GithubActionsPlugin + importName: EntityGithubActionsContent + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isGithubActionsAvailable + backstage.plugin-github-issues: + mountPoints: + - mountPoint: entity.page.issues/cards + module: GithubIssuesPlugin + importName: GithubIssuesCard + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - hasAnnotation: github.com/project-slug + backstage.plugin-jenkins: + mountPoints: + - mountPoint: entity.page.ci/cards + module: JenkinsPlugin + importName: EntityJenkinsContent + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isJenkinsAvailable + backstage.plugin-kubernetes: + mountPoints: + - mountPoint: entity.page.kubernetes/cards + module: KubernetesPlugin + importName: EntityKubernetesContent + config: + layout: + gridColumn: "1 / -1" + if: + anyOf: + - hasAnnotation: backstage.io/kubernetes-id + - hasAnnotation: backstage.io/kubernetes-namespace + backstage.plugin-lighthouse: + dynamicRoutes: + - path: /lighthouse + module: LighthousePlugin + importName: LighthousePage + menuItem: + icon: Assessment + text: Lighthouse + mountPoints: + - mountPoint: entity.page.overview/cards + module: LighthousePlugin + importName: EntityLastLighthouseAuditCard + config: + layout: + gridColumnEnd: "span 6" + if: + allOf: + - isLighthouseAvailable + backstage.plugin-pagerduty: + mountPoints: + - mountPoint: entity.page.overview/cards + module: PagerdutyPlugin + importName: EntityPagerDutyCard + config: + layout: + gridColumnEnd: "span 6" + if: + allOf: + - isPluginApplicableToEntity + backstage.plugin-sonarqube: + mountPoints: + - mountPoint: entity.page.overview/cards + module: SonarQubePlugin + importName: EntitySonarQubeCard + config: + layout: + gridColumnStart: "span 4" + gridRowEnd: "span 2" + if: + allOf: + - isSonarQubeAvailable + immobiliarelabs.backstage-plugin-gitlab: + mountPoints: + - mountPoint: entity.page.overview/cards + module: GitlabPlugin + importName: EntityGitlabMergeRequestStatsCard + config: + layout: + gridRowEnd: "span 2" + gridColumnStart: "span 4" + if: + allOf: + - isGitlabAvailable + - mountPoint: entity.page.ci/cards + module: GitlabPlugin + importName: EntityGitlabPipelinesTable + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isGitlabAvailable + - mountPoint: entity.page.issues/cards + module: GitlabPlugin + importName: EntityGitlabIssuesTable + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isGitlabAvailable + - mountPoint: entity.page.pull-requests/cards + module: GitlabPlugin + importName: EntityGitlabMergeRequestsTable + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isGitlabAvailable + janus-idp.backstage-plugin-jfrog-artifactory: + mountPoints: + - mountPoint: entity.page.image-registry/cards + module: JfrogArtifactoryPlugin + importName: JfrogArtifactoryPage + config: + layout: + gridColumn: 1 / -1 + if: + anyOf: + - isJfrogArtifactoryAvailable + janus-idp.backstage-plugin-nexus-repository-manager: + mountPoints: + - mountPoint: entity.page.image-registry/cards + module: NexusRepositoryManagerPlugin + importName: NexusRepositoryManagerPage + config: + layout: + gridColumn: 1 / -1 + if: + anyOf: + - isNexusRepositoryManagerAvailable + janus-idp.backstage-plugin-ocm: + dynamicRoutes: + - path: /ocm + module: OcmPlugin + importName: OcmPage + menuItem: + icon: Storage + text: Clusters + mountPoints: + - mountPoint: entity.page.overview/context + module: OcmPlugin + importName: ClusterContextProvider + - mountPoint: entity.page.overview/cards + module: OcmPlugin + importName: ClusterAvailableResourceCard + config: + layout: + gridRowStart: 1 + gridColumnStart: "span 1" + if: + anyOf: + - isKind: resource + - isType: kubernetes-cluster + - mountPoint: entity.page.overview/cards + importName: ClusterInfoCard + module: OcmPlugin + config: + if: + allOf: + - isKind: resource + - isType: kubernetes-cluster + janus-idp.backstage-plugin-quay: + mountPoints: + - mountPoint: entity.page.image-registry/cards + module: QuayPlugin + importName: QuayPage + config: + layout: + gridColumn: 1 / -1 + if: + anyOf: + - isQuayAvailable + janus-idp.backstage-plugin-tekton: + mountPoints: + - mountPoint: entity.page.ci/cards + module: TektonPlugin + importName: TektonCI + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isTektonCIAvailable + janus-idp.backstage-plugin-topology: + mountPoints: + - mountPoint: entity.page.topology/cards + module: TopologyPlugin + importName: TopologyPage + config: + layout: + gridColumn: "1 / -1" + if: + anyOf: + - hasAnnotation: backstage.io/kubernetes-id + - hasAnnotation: backstage.io/kubernetes-namespace + roadiehq.backstage-plugin-argo-cd: + mountPoints: + - mountPoint: entity.page.overview/cards + module: ArgocdPlugin + importName: EntityArgoCDOverviewCard + config: + layout: + gridColumn: "1 / span 8" + if: + allOf: + - isArgocdAvailable + - mountPoint: entity.page.cd/cards + module: ArgocdPlugin + importName: EntityArgoCDHistoryCard + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isArgocdAvailable + roadiehq.backstage-plugin-datadog: + mountPoints: + - mountPoint: entity.page.monitoring/cards + module: DatadogPlugin + importName: EntityDatadogContent + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isDatadogAvailable + roadiehq.backstage-plugin-github-insights: + mountPoints: + - mountPoint: entity.page.overview/cards + module: GithubInsightsPlugin + importName: EntityGithubInsightsComplianceCard + config: + layout: + gridRowEnd: "span 2" + gridColumnStart: "span 4" + if: + allOf: + - isGithubInsightsAvailable + roadiehq.backstage-plugin-github-pull-requests: + mountPoints: + - mountPoint: entity.page.overview/cards + module: GithubPullRequestsPlugin + importName: EntityGithubPullRequestsOverviewCard + config: + layout: + gridRow: "1 / span 2" + gridColumn: "5 / span 4" + if: + allOf: + - isGithubPullRequestsAvailable + - mountPoint: entity.page.pull-requests/cards + module: GithubPullRequestsPlugin + importName: EntityGithubPullRequestsContent + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isGithubPullRequestsAvailable + roadiehq.backstage-plugin-jira: + mountPoints: + - mountPoint: entity.page.issues/cards + module: JiraPlugin + importName: EntityJiraOverviewCard + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isJiraAvailable + roadiehq.backstage-plugin-security-insights: + mountPoints: + - mountPoint: entity.page.overview/cards + module: SecurityInsightsPlugin + importName: EntityDependabotAlertsCard + config: + layout: + gridRowEnd: "span 2" + gridColumnStart: "span 4" + if: + allOf: + - isSecurityInsightsAvailable diff --git a/packages/app/config.d.ts b/packages/app/config.d.ts index e8732874c3..362344b162 100644 --- a/packages/app/config.d.ts +++ b/packages/app/config.d.ts @@ -65,7 +65,7 @@ export interface Config { text: string; }; })[]; - routeBindings: { + routeBindings?: { bindTarget: string; bindMap: { [key: string]: string; @@ -74,8 +74,36 @@ export interface Config { mountPoints: { mountPoint: string; module: string; - importName?: string; - }[]; + importName: string; + config: { + layout?: { + [key: string]: string; + }; + props?: { + [key: string]: string; + }; + if?: { + allOf?: ( + | { + [key: string]: string | string[]; + } + | string + )[]; + anyOf?: ( + | { + [key: string]: string | string[]; + } + | string + )[]; + oneOf?: ( + | { + [key: string]: string | string[]; + } + | string + )[]; + }; + }[]; + }; }; }; }; diff --git a/packages/app/src/components/AppBase/AppBase.tsx b/packages/app/src/components/AppBase/AppBase.tsx index 1a3f969cb7..738d33b140 100644 --- a/packages/app/src/components/AppBase/AppBase.tsx +++ b/packages/app/src/components/AppBase/AppBase.tsx @@ -17,7 +17,6 @@ import { import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { UserSettingsPage } from '@backstage/plugin-user-settings'; -import { OcmPage } from '@janus-idp/backstage-plugin-ocm'; import React, { useContext } from 'react'; import { Route } from 'react-router-dom'; import { Root } from '../Root'; @@ -25,7 +24,6 @@ import { entityPage } from '../catalog/EntityPage'; import { HomePage } from '../home/HomePage'; import { LearningPaths } from '../learningPaths/LearningPathsPage'; import { SearchPage } from '../search/SearchPage'; -import { LighthousePage } from '@backstage/plugin-lighthouse'; import DynamicRootContext from '../DynamicRoot/DynamicRootContext'; const AppBase = () => { @@ -83,9 +81,7 @@ const AppBase = () => { } /> } /> - } /> } /> - } /> {dynamicRoutes.map(({ Component, path, ...props }) => ( { + if (typeof condition === 'function') { + return (entity: Entity) => Boolean(condition(entity)); + } + if (condition.isKind) { + return isKind(condition.isKind); + } + if (condition.isType) { + return isType(condition.isType); + } + if (condition.hasAnnotation) { + return hasAnnotation(condition.hasAnnotation as string); + } + return () => false; +}; + +const configIfToCallable = + (conditional: ScalprumMountPointConfigRawIf) => (e: Entity) => { + if (conditional?.allOf) { + return conditional.allOf.map(conditionsArrayMapper).every(f => f(e)); + } + if (conditional?.anyOf) { + return conditional.anyOf.map(conditionsArrayMapper).some(f => f(e)); + } + if (conditional?.oneOf) { + return ( + conditional.oneOf.map(conditionsArrayMapper).filter(f => f(e)) + .length === 1 + ); + } + return true; + }; const DynamicRoot = ({ afterInit, @@ -85,9 +128,22 @@ const DynamicRoot = ({ } const providerMountPoints = mountPoints.map( - ({ module, importName, mountPoint, scope }) => ({ + ({ module, importName, mountPoint, scope, config }) => ({ mountPoint, Component: remotePlugins[scope][module][importName], + config: { + ...config, + if: configIfToCallable( + Object.fromEntries( + Object.entries(config?.if || {}).map(([k, v]) => [ + k, + v.map(c => + typeof c === 'string' ? remotePlugins[scope][module][c] : c, + ), + ]), + ), + ), + }, }), ); @@ -97,7 +153,10 @@ const DynamicRoot = ({ if (!acc[entry.mountPoint]) { acc[entry.mountPoint] = []; } - acc[entry.mountPoint].push(entry.Component); + acc[entry.mountPoint].push({ + component: entry.Component, + config: entry.config, + }); return acc; }, {}); getScalprum().api.mountPoints = mountPointComponents; diff --git a/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx b/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx index ec54ffe4ec..4bd4f33255 100644 --- a/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx +++ b/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx @@ -1,6 +1,7 @@ import React, { createContext } from 'react'; import { ScalprumComponentProps } from '@scalprum/react-core'; +import { Entity } from '@backstage/catalog-model'; export type RouteBinding = { bindTarget: string; @@ -24,7 +25,32 @@ export type DynamicRootContextValue = DynamicModuleEntry & { Component: React.ComponentType; }; -export type ScalprumMountPoint = React.ComponentType<{}>; +type ScalprumMountPointConfigBase = { + layout?: Record; + props?: Record; +}; + +export type ScalprumMountPointConfig = ScalprumMountPointConfigBase & { + if: (e: Entity) => boolean | Promise; +}; + +export type ScalprumMountPointConfigRawIf = { + [key in 'allOf' | 'oneOf' | 'anyOf']?: ( + | { + [key: string]: string | string[]; + } + | Function + )[]; +}; + +export type ScalprumMountPointConfigRaw = ScalprumMountPointConfigBase & { + if?: ScalprumMountPointConfigRawIf; +}; + +export type ScalprumMountPoint = { + component: React.ComponentType<{}>; + config?: ScalprumMountPointConfig; +}; export type RemotePlugins = { [scope: string]: { diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index fd60fe5d2a..1348df04f6 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -21,7 +21,6 @@ import MenuIcon from '@mui/icons-material/Menu'; import MapIcon from '@mui/icons-material/MyLocation'; import SchoolIcon from '@mui/icons-material/School'; import SearchIcon from '@mui/icons-material/Search'; -import AssessmentIcon from '@mui/icons-material/Assessment'; import React, { PropsWithChildren, useContext } from 'react'; import { SidebarLogo } from './SidebarLogo'; import DynamicRootContext from '../DynamicRoot/DynamicRootContext'; @@ -61,11 +60,6 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { to="tech-radar" text="Tech Radar" /> - {dynamicRoutes.map(({ menuItem, path }) => { if (menuItem) { return ( diff --git a/packages/app/src/components/catalog/EntityPage/Content/CD.tsx b/packages/app/src/components/catalog/EntityPage/Content/CD.tsx deleted file mode 100644 index c14061d2ae..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/CD.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Grid from '@mui/material/Grid'; -import { EntityArgoCDHistoryCard } from '@roadiehq/backstage-plugin-argo-cd'; -import React from 'react'; - -export const cdContent = ( - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/CI.tsx b/packages/app/src/components/catalog/EntityPage/Content/CI.tsx deleted file mode 100644 index 7f3d407b9a..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/CI.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { type Entity } from '@backstage/catalog-model'; -import { - EntityAzurePipelinesContent, - isAzureDevOpsAvailable, -} from '@backstage/plugin-azure-devops'; -import { EntitySwitch } from '@backstage/plugin-catalog'; -import { - EntityGithubActionsContent, - isGithubActionsAvailable, -} from '@backstage/plugin-github-actions'; -import { - EntityGitlabPipelinesTable, - isGitlabAvailable, -} from '@immobiliarelabs/backstage-plugin-gitlab'; -import { - TektonCI, - isTektonCIAvailable, -} from '@janus-idp/backstage-plugin-tekton'; -import Grid from '@mui/material/Grid'; -import React from 'react'; - -const ifCIs: ((e: Entity) => boolean)[] = [ - isGithubActionsAvailable, - isGitlabAvailable, - isTektonCIAvailable, - isAzureDevOpsAvailable, -]; - -export const isCIsAvailable = (e: Entity) => ifCIs.some(f => f(e)); - -export const ciContent = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/Dependencies.tsx b/packages/app/src/components/catalog/EntityPage/Content/Dependencies.tsx deleted file mode 100644 index 7ddf8b0273..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/Dependencies.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - EntityConsumedApisCard, - EntityProvidedApisCard, -} from '@backstage/plugin-api-docs'; -import { - EntityDependsOnComponentsCard, - EntityDependsOnResourcesCard, - EntityHasSubcomponentsCard, -} from '@backstage/plugin-catalog'; -import { - Direction, - EntityCatalogGraphCard, -} from '@backstage/plugin-catalog-graph'; -import Grid from '@mui/material/Grid'; -import React from 'react'; - -export const dependenciesContent = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/Docs.tsx b/packages/app/src/components/catalog/EntityPage/Content/Docs.tsx deleted file mode 100644 index 2f0b62a639..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/Docs.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; -import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; -import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; -import Grid from '@mui/material/Grid'; -import React from 'react'; - -export const techdocsContent = ( - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/EntityWarning.tsx b/packages/app/src/components/catalog/EntityPage/Content/EntityWarning.tsx deleted file mode 100644 index ef72595b99..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/EntityWarning.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { - EntityOrphanWarning, - EntityProcessingErrorsPanel, - EntityRelationWarning, - EntitySwitch, - hasCatalogProcessingErrors, - hasRelationWarnings, - isOrphan, -} from '@backstage/plugin-catalog'; -import Grid from '@mui/material/Grid'; -import React from 'react'; - -export const entityWarningContent = ( - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/ImageRegistry.tsx b/packages/app/src/components/catalog/EntityPage/Content/ImageRegistry.tsx deleted file mode 100644 index e136c9e57c..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/ImageRegistry.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { type Entity } from '@backstage/catalog-model'; -import { EntitySwitch } from '@backstage/plugin-catalog'; -import { - JfrogArtifactoryPage, - isJfrogArtifactoryAvailable, -} from '@janus-idp/backstage-plugin-jfrog-artifactory'; -import { QuayPage, isQuayAvailable } from '@janus-idp/backstage-plugin-quay'; -import { - isNexusRepositoryManagerAvailable, - NexusRepositoryManagerPage, -} from '@janus-idp/backstage-plugin-nexus-repository-manager'; -import Grid from '@mui/material/Grid'; -import React from 'react'; - -const ifImageRegistries: ((e: Entity) => boolean)[] = [ - isQuayAvailable, - isJfrogArtifactoryAvailable, -]; - -export const isImageRegistriesAvailable = (e: Entity) => - ifImageRegistries.some(f => f(e)); - -export const imageRegistry = ( - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/Issues.tsx b/packages/app/src/components/catalog/EntityPage/Content/Issues.tsx deleted file mode 100644 index 99a2ec33b3..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/Issues.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { type Entity } from '@backstage/catalog-model'; -import { EntitySwitch } from '@backstage/plugin-catalog'; -import { GithubIssuesCard } from '@backstage/plugin-github-issues'; -import { - EntityGitlabIssuesTable, - isGitlabAvailable, -} from '@immobiliarelabs/backstage-plugin-gitlab'; -import Grid from '@mui/material/Grid'; -import { isGithubPullRequestsAvailable } from '@roadiehq/backstage-plugin-github-pull-requests'; -import { - EntityJiraOverviewCard, - isJiraAvailable, -} from '@roadiehq/backstage-plugin-jira'; -import React from 'react'; - -const ifIssues: ((e: Entity) => boolean)[] = [ - isGithubPullRequestsAvailable, - isGitlabAvailable, - isJiraAvailable, -]; - -export const isIssuesAvailable = (e: Entity) => ifIssues.some(f => f(e)); - -export const issuesContent = ( - - - - - - - - - - - - - - - {/* TODO: update GithubIssuesCard if entity check once its available */} - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/Monitoring.tsx b/packages/app/src/components/catalog/EntityPage/Content/Monitoring.tsx deleted file mode 100644 index 73a5603be8..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/Monitoring.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { type Entity } from '@backstage/catalog-model'; -import { EntitySwitch } from '@backstage/plugin-catalog'; -import Grid from '@mui/material/Grid'; -import { - DynatraceTab, - isDynatraceAvailable, -} from '@backstage/plugin-dynatrace'; -import { - EntityDatadogContent, - isDatadogAvailable, -} from '@roadiehq/backstage-plugin-datadog'; -import React from 'react'; - -const ifMonitoring: ((e: Entity) => boolean)[] = [ - isDatadogAvailable, - isDynatraceAvailable, -]; - -export const isMonitoringAvailable = (e: Entity) => - ifMonitoring.some(f => f(e)); - -export const monitoringContent = ( - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/Overview.tsx b/packages/app/src/components/catalog/EntityPage/Content/Overview.tsx deleted file mode 100644 index 12db9040c4..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/Overview.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { EmptyState } from '@backstage/core-components'; -import { - EntityAboutCard, - EntityLinksCard, - EntitySwitch, -} from '@backstage/plugin-catalog'; -import Grid from '@mui/material/Grid'; -import React from 'react'; - -import { - EntityLatestJenkinsRunCard, - isJenkinsAvailable, -} from '@backstage/plugin-jenkins'; -import { EntitySonarQubeCard } from '@backstage/plugin-sonarqube'; -import { isSonarQubeAvailable } from '@backstage/plugin-sonarqube-react'; -import { - EntityGitlabMergeRequestStatsCard, - isGitlabAvailable, -} from '@immobiliarelabs/backstage-plugin-gitlab'; -import { EntityArgoCDOverviewCard } from '@roadiehq/backstage-plugin-argo-cd'; -import { - EntityGithubInsightsComplianceCard, - isGithubInsightsAvailable, -} from '@roadiehq/backstage-plugin-github-insights'; -import { - EntityGithubPullRequestsOverviewCard, - isGithubPullRequestsAvailable, -} from '@roadiehq/backstage-plugin-github-pull-requests'; -import { - EntityDependabotAlertsCard, - isSecurityInsightsAvailable, -} from '@roadiehq/backstage-plugin-security-insights'; -import { - isPluginApplicableToEntity as isPagerDutyAvailable, - EntityPagerDutyCard, -} from '@backstage/plugin-pagerduty'; -import { - EntityLastLighthouseAuditCard, - isLighthouseAvailable, -} from '@backstage/plugin-lighthouse'; -import { isCIsAvailable } from './CI'; -import { entityWarningContent } from './EntityWarning'; -import { isPRsAvailable } from './PullRequests'; - -export const overviewContent = ( - - - {entityWarningContent} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - !isPRsAvailable(e)}> - - - - - - - - - - - - - - - - - - - - - - - {/* Use `isArgocdAvailable` once its fixed */} - - - - - - - - - - - - - - - -   - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/PullRequests.tsx b/packages/app/src/components/catalog/EntityPage/Content/PullRequests.tsx deleted file mode 100644 index f85288cae8..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/PullRequests.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { type Entity } from '@backstage/catalog-model'; -import { - EntityAzurePullRequestsContent, - isAzureDevOpsAvailable, -} from '@backstage/plugin-azure-devops'; -import { EntitySwitch } from '@backstage/plugin-catalog'; -import { - EntityGitlabMergeRequestsTable, - isGitlabAvailable, -} from '@immobiliarelabs/backstage-plugin-gitlab'; -import Grid from '@mui/material/Grid'; -import { - EntityGithubPullRequestsContent, - isGithubPullRequestsAvailable, -} from '@roadiehq/backstage-plugin-github-pull-requests'; -import React from 'react'; - -const ifPRs: ((e: Entity) => boolean)[] = [ - isGithubPullRequestsAvailable, - isGitlabAvailable, - isAzureDevOpsAvailable, -]; - -export const isPRsAvailable = (e: Entity) => ifPRs.some(f => f(e)); - -export const prContent = ( - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/SecurityInsights.tsx b/packages/app/src/components/catalog/EntityPage/Content/SecurityInsights.tsx deleted file mode 100644 index 3ce67c9c30..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/SecurityInsights.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import Grid from '@mui/material/Grid'; -import { - EntityGithubDependabotContent, - EntitySecurityInsightsContent, -} from '@roadiehq/backstage-plugin-security-insights'; -import React from 'react'; - -export const securityContent = ( - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Content/index.tsx b/packages/app/src/components/catalog/EntityPage/Content/index.tsx deleted file mode 100644 index ff4cbc2073..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Content/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export * from './CD'; -export * from './CI'; -export * from './Dependencies'; -export * from './Docs'; -export * from './ImageRegistry'; -export * from './Issues'; -export * from './Monitoring'; -export * from './Overview'; -export * from './PullRequests'; -export * from './SecurityInsights'; diff --git a/packages/app/src/components/catalog/EntityPage/Pages/Api.tsx b/packages/app/src/components/catalog/EntityPage/Pages/Api.tsx deleted file mode 100644 index 033ab60154..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/Api.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - EntityApiDefinitionCard, - EntityConsumingComponentsCard, - EntityProvidingComponentsCard, -} from '@backstage/plugin-api-docs'; -import { - EntityAboutCard, - EntityLayout, - EntityLinksCard, -} from '@backstage/plugin-catalog'; -import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { entityWarningContent } from '../Content/EntityWarning'; - -export const apiPage = ( - - - - - {entityWarningContent} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Pages/Component.tsx b/packages/app/src/components/catalog/EntityPage/Pages/Component.tsx deleted file mode 100644 index dea2a70878..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/Component.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { - EntityConsumedApisCard, - EntityProvidedApisCard, -} from '@backstage/plugin-api-docs'; -import { - EntityLayout, - EntitySwitch, - isComponentType, -} from '@backstage/plugin-catalog'; -import { EntityKubernetesContent } from '@backstage/plugin-kubernetes'; -import { - EntityLighthouseContent, - isLighthouseAvailable, -} from '@backstage/plugin-lighthouse'; -import { TektonCI } from '@janus-idp/backstage-plugin-tekton'; -import { TopologyPage } from '@janus-idp/backstage-plugin-topology'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { - cdContent, - ciContent, - dependenciesContent, - imageRegistry, - isCIsAvailable, - isImageRegistriesAvailable, - isIssuesAvailable, - isMonitoringAvailable, - isPRsAvailable, - issuesContent, - monitoringContent, - overviewContent, - prContent, - techdocsContent, -} from '../Content'; -import { defaultEntityPage } from './DefaultEntity'; - -const componentEntityPage = (componentType: 'service' | 'website') => ( - - - {overviewContent} - - - - - - - - {issuesContent} - - - - {prContent} - - - - {ciContent} - - - {/* Use `isArgocdAvailable` once its fixed */} - - {cdContent} - - - - - - - - - - - - {imageRegistry} - - - - {monitoringContent} - - - - - - - {componentType === 'service' && ( - - - - - - - - - - - )} - - - {dependenciesContent} - - - - {techdocsContent} - - -); - -export const componentPage = ( - - - {componentEntityPage('service')} - - - - {componentEntityPage('website')} - - - {defaultEntityPage} - -); diff --git a/packages/app/src/components/catalog/EntityPage/Pages/DefaultEntity.tsx b/packages/app/src/components/catalog/EntityPage/Pages/DefaultEntity.tsx deleted file mode 100644 index 20282c2b32..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/DefaultEntity.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { EntityLayout } from '@backstage/plugin-catalog'; -import React from 'react'; -import { overviewContent, techdocsContent } from '../Content'; - -export const defaultEntityPage = ( - - - {overviewContent} - - - - {techdocsContent} - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Pages/Domain.tsx b/packages/app/src/components/catalog/EntityPage/Pages/Domain.tsx deleted file mode 100644 index d87730ef0f..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/Domain.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { - EntityAboutCard, - EntityHasSystemsCard, - EntityLayout, -} from '@backstage/plugin-catalog'; -import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { entityWarningContent } from '../Content/EntityWarning'; - -export const domainPage = ( - - - - - {entityWarningContent} - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Pages/Group.tsx b/packages/app/src/components/catalog/EntityPage/Pages/Group.tsx deleted file mode 100644 index f5664e2952..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/Group.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { EntityLayout } from '@backstage/plugin-catalog'; -import { - EntityGroupProfileCard, - EntityMembersListCard, - EntityOwnershipCard, -} from '@backstage/plugin-org'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { entityWarningContent } from '../Content/EntityWarning'; - -export const groupPage = ( - - - - - {entityWarningContent} - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Pages/Resource.tsx b/packages/app/src/components/catalog/EntityPage/Pages/Resource.tsx deleted file mode 100644 index 3f7d51e33a..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/Resource.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { - EntityAboutCard, - EntityHasSystemsCard, - EntityLayout, - EntityLinksCard, - EntitySwitch, -} from '@backstage/plugin-catalog'; -import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; -import Grid from '@mui/material/Grid'; -import React from 'react'; - -import { - ClusterAvailableResourceCard, - ClusterContextProvider, - ClusterInfoCard, -} from '@janus-idp/backstage-plugin-ocm'; -import { isType } from '../../utils'; -import { entityWarningContent } from '../Content/EntityWarning'; - -export const resourcePage = ( - - - - - {entityWarningContent} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Pages/System.tsx b/packages/app/src/components/catalog/EntityPage/Pages/System.tsx deleted file mode 100644 index ef261b498f..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/System.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { - RELATION_API_CONSUMED_BY, - RELATION_API_PROVIDED_BY, - RELATION_CONSUMES_API, - RELATION_DEPENDENCY_OF, - RELATION_DEPENDS_ON, - RELATION_HAS_PART, - RELATION_PART_OF, - RELATION_PROVIDES_API, -} from '@backstage/catalog-model'; -import { EntityHasApisCard } from '@backstage/plugin-api-docs'; -import { - EntityAboutCard, - EntityHasComponentsCard, - EntityHasResourcesCard, - EntityLayout, - EntityLinksCard, -} from '@backstage/plugin-catalog'; -import { - Direction, - EntityCatalogGraphCard, -} from '@backstage/plugin-catalog-graph'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { entityWarningContent } from '../Content/EntityWarning'; - -export const systemPage = ( - - - - - {entityWarningContent} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Pages/User.tsx b/packages/app/src/components/catalog/EntityPage/Pages/User.tsx deleted file mode 100644 index b4ec1f9c6d..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/User.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { EntityLayout } from '@backstage/plugin-catalog'; -import { - EntityOwnershipCard, - EntityUserProfileCard, -} from '@backstage/plugin-org'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { entityWarningContent } from '../Content/EntityWarning'; - -export const userPage = ( - - - - - {entityWarningContent} - - - - - - - - - - - - -); diff --git a/packages/app/src/components/catalog/EntityPage/Pages/index.tsx b/packages/app/src/components/catalog/EntityPage/Pages/index.tsx deleted file mode 100644 index 6f03788200..0000000000 --- a/packages/app/src/components/catalog/EntityPage/Pages/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export * from './Api'; -export * from './Component'; -export * from './DefaultEntity'; -export * from './Domain'; -export * from './Group'; -export * from './Resource'; -export * from './System'; -export * from './User'; diff --git a/packages/app/src/components/catalog/EntityPage/index.tsx b/packages/app/src/components/catalog/EntityPage/index.tsx index a0179b656a..a882b7138e 100644 --- a/packages/app/src/components/catalog/EntityPage/index.tsx +++ b/packages/app/src/components/catalog/EntityPage/index.tsx @@ -1,27 +1,352 @@ -import { EntitySwitch, isKind } from '@backstage/plugin-catalog'; import React from 'react'; - import { - componentPage, - defaultEntityPage, - apiPage, - groupPage, - userPage, - systemPage, - domainPage, - resourcePage, -} from './Pages'; + EntityApiDefinitionCard, + EntityConsumedApisCard, + EntityConsumingComponentsCard, + EntityHasApisCard, + EntityProvidedApisCard, + EntityProvidingComponentsCard, +} from '@backstage/plugin-api-docs'; +import { + EntityAboutCard, + EntityDependsOnComponentsCard, + EntityDependsOnResourcesCard, + EntityHasComponentsCard, + EntityHasResourcesCard, + EntityHasSubcomponentsCard, + EntityHasSystemsCard, + EntityLayout, + EntityLinksCard, + EntityOrphanWarning, + EntityProcessingErrorsPanel, + EntityRelationWarning, + EntitySwitch, + hasCatalogProcessingErrors, + hasRelationWarnings, + isKind, + isOrphan, +} from '@backstage/plugin-catalog'; +import tab from '../tab'; +import { hasLinks, isType } from '../utils'; +import { + Direction, + EntityCatalogGraphCard, +} from '@backstage/plugin-catalog-graph'; +import { + EntityTechdocsContent, + isTechDocsAvailable, +} from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { + EntityGroupProfileCard, + EntityMembersListCard, + EntityOwnershipCard, + EntityUserProfileCard, +} from '@backstage/plugin-org'; +import { + RELATION_API_CONSUMED_BY, + RELATION_API_PROVIDED_BY, + RELATION_CONSUMES_API, + RELATION_DEPENDENCY_OF, + RELATION_DEPENDS_ON, + RELATION_HAS_PART, + RELATION_PART_OF, + RELATION_PROVIDES_API, +} from '@backstage/catalog-model'; +import Grid from '../Grid'; + +// import {EntityGithubPullRequestsContent} from '@roadiehq/backstage-plugin-github-pull-requests'; export const entityPage = ( - - - - - - - - - - {defaultEntityPage} - + + {tab({ + path: '/', + title: 'Overview', + mountPoint: 'entity.page.overview', + children: ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + })} + + {tab({ + path: '/topology', + title: 'Topology', + mountPoint: 'entity.page.topology', + })} + + {tab({ + path: '/issues', + title: 'Issues', + mountPoint: 'entity.page.issues', + })} + + {tab({ + path: '/pr', + title: 'Pull/Merge Requests', + mountPoint: 'entity.page.pull-requests', + // children: ( + // + // + // + // ) + })} + + {tab({ + path: '/ci', + title: 'CI', + mountPoint: 'entity.page.ci', + })} + + {tab({ + path: '/cd', + title: 'CD', + mountPoint: 'entity.page.cd', + })} + + {tab({ + path: '/kubernetes', + title: 'Kubernetes', + mountPoint: 'entity.page.kubernetes', + })} + + {tab({ + path: '/tekton', + title: 'Tekton', + mountPoint: 'entity.page.tekton', + })} + + {tab({ + path: '/image-registry', + title: 'Image Registry', + mountPoint: 'entity.page.image-registry', + })} + + {tab({ + path: '/monitoring', + title: 'Monitoring', + mountPoint: 'entity.page.monitoring', + })} + + {tab({ + path: '/lighthouse', + title: 'Lighthouse', + mountPoint: 'entity.page.lighthouse', + })} + + {tab({ + path: '/api', + title: 'Api', + mountPoint: 'entity.page.api', + if: e => isType('service')(e) && isKind('component')(e), + children: ( + + isType('service')(e) && isKind('component')(e)} + > + + + + + + + + + ), + })} + + {tab({ + path: '/dependencies', + title: 'Dependencies', + mountPoint: 'entity.page.dependencies', + if: isKind('component'), + children: ( + + + + + + + + + + + + + + + + + + + + + + + ), + })} + + {tab({ + path: '/docs', + title: 'Docs', + mountPoint: 'entity.page.docs', + if: isTechDocsAvailable, + children: ( + + + + + + + + + + + + ), + })} + + {tab({ + path: '/definition', + title: 'Definition', + mountPoint: 'entity.page.definition', + if: isKind('api'), + children: ( + + + + + + + + ), + })} + + {tab({ + path: '/diagram', + title: 'Diagram', + mountPoint: 'entity.page.diagram', + if: isKind('system'), + children: ( + + + + + + + + ), + })} + ); diff --git a/packages/app/src/components/catalog/Grid/Grid.tsx b/packages/app/src/components/catalog/Grid/Grid.tsx new file mode 100644 index 0000000000..8e9ad36fbe --- /dev/null +++ b/packages/app/src/components/catalog/Grid/Grid.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import Box, { BoxProps } from '@mui/material/Box'; +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles()(theme => ({ + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(12, 1fr)', + gridGap: theme.spacing(3), + gridAutoFlow: 'dense', + }, +})); + +const Grid = ({ + container = false, + item = true, + children, + ...props +}: React.PropsWithChildren< + { container?: boolean; item?: boolean } & BoxProps +>) => { + const { classes } = useStyles(); + + if (container) { + return ( + + {children} + + ); + } + if (item) { + return {children}; + } + return null; +}; + +export default Grid; diff --git a/packages/app/src/components/catalog/Grid/index.ts b/packages/app/src/components/catalog/Grid/index.ts new file mode 100644 index 0000000000..3d919c9acb --- /dev/null +++ b/packages/app/src/components/catalog/Grid/index.ts @@ -0,0 +1 @@ +export { default } from './Grid'; diff --git a/packages/app/src/components/catalog/tab/index.ts b/packages/app/src/components/catalog/tab/index.ts new file mode 100644 index 0000000000..bd1dff4eb0 --- /dev/null +++ b/packages/app/src/components/catalog/tab/index.ts @@ -0,0 +1 @@ +export { default } from './tab'; diff --git a/packages/app/src/components/catalog/tab/tab.tsx b/packages/app/src/components/catalog/tab/tab.tsx new file mode 100644 index 0000000000..bd940c427a --- /dev/null +++ b/packages/app/src/components/catalog/tab/tab.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import getMountPointData from '../../../utils/dynamicUI/getMountPointData'; +import { EntityLayout, EntitySwitch } from '@backstage/plugin-catalog'; +import Grid from '../Grid'; +import { Entity } from '@backstage/catalog-model'; + +export type TabProps = { + path: string; + title: string; + mountPoint: string; + if?: (e: Entity) => boolean; + children?: React.ReactNode; +}; + +const tab = ({ + path, + title, + mountPoint, + children, + if: condition, +}: TabProps) => ( + + (condition ? condition(e) : Boolean(children)) || + getMountPointData(`${mountPoint}/cards`) + .flatMap(({ config }) => config.if) + .some(c => c(e)) + } + > + {getMountPointData(`${mountPoint}/context`).reduce( + (acc, { component: Component }) => ( + {acc} + ), + + {children} + {getMountPointData(`${mountPoint}/cards`).map( + ({ component: Component, config }, index) => ( + + +
+ +
+
+
+ ), + )} +
, + )} +
+); + +export default tab; diff --git a/packages/app/src/components/catalog/utils.tsx b/packages/app/src/components/catalog/utils.tsx index af4898acc9..ee4c683027 100644 --- a/packages/app/src/components/catalog/utils.tsx +++ b/packages/app/src/components/catalog/utils.tsx @@ -8,3 +8,8 @@ export const isType = (types: string | string[]) => (entity: Entity) => { ? entity?.spec?.type === types : types.includes(entity.spec.type as string); }; +export const hasAnnotation = (keys: string) => (entity: Entity) => + Boolean(entity.metadata.annotations?.[keys]); + +export const hasLinks = (entity: Entity) => + Boolean(entity.metadata.links?.length); diff --git a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts index 537e73fb73..a34431b3bd 100644 --- a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts +++ b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts @@ -2,6 +2,7 @@ import { defaultConfigLoader } from '@backstage/core-app-api'; import { DynamicModuleEntry, RouteBinding, + ScalprumMountPointConfigRaw, } from '../../components/DynamicRoot/DynamicRootContext'; type AppConfig = { @@ -48,6 +49,7 @@ async function extractDynamicConfig() { module: string; importName: string; mountPoint: string; + config?: ScalprumMountPointConfigRaw; }[]; }>( (acc, { data }) => { diff --git a/packages/app/src/utils/dynamicUI/getMountPointData.ts b/packages/app/src/utils/dynamicUI/getMountPointData.ts index ab1dcf4f6c..0e17888134 100644 --- a/packages/app/src/utils/dynamicUI/getMountPointData.ts +++ b/packages/app/src/utils/dynamicUI/getMountPointData.ts @@ -1,6 +1,9 @@ import { getScalprum } from '@scalprum/core'; +import { ScalprumMountPointConfig } from '../../components/DynamicRoot/DynamicRootContext'; -function getMountPointData(mountPoint: string): T[] { +function getMountPointData( + mountPoint: string, +): { config: ScalprumMountPointConfig; component: T }[] { return getScalprum().api.mountPoints?.[mountPoint] ?? []; }