From 408539a43b820cc53b4ed9eb4f79f7fe75bbd4db Mon Sep 17 00:00:00 2001 From: Pankaj Walke Date: Wed, 10 Jun 2026 16:15:10 +0000 Subject: [PATCH] Apply PEEKS patch and fix homepage links --- packages/app/config.d.ts | 33 +++ packages/app/package.json | 4 +- packages/app/src/App.tsx | 6 +- .../scaffolder/CustomScaffolderPage.tsx | 269 ++++++++++++++++++ .../scaffolder/ScaffolderFilterDropdown.tsx | 115 ++++++++ packages/backend/package.json | 3 +- packages/backend/src/index.ts | 1 + .../backend/src/modules/actions/k8s-apply.ts | 41 ++- packages/backend/src/modules/scaffolder.ts | 1 + 9 files changed, 464 insertions(+), 9 deletions(-) create mode 100644 packages/app/config.d.ts create mode 100644 packages/app/src/components/scaffolder/CustomScaffolderPage.tsx create mode 100644 packages/app/src/components/scaffolder/ScaffolderFilterDropdown.tsx diff --git a/packages/app/config.d.ts b/packages/app/config.d.ts new file mode 100644 index 0000000..c3eefc6 --- /dev/null +++ b/packages/app/config.d.ts @@ -0,0 +1,33 @@ +export interface Config { + /** Homepage configuration + * @visibility frontend + */ + homepage?: { + /** + * Quick links displayed on the homepage + * @visibility frontend + */ + quickLinks?: Array<{ + /** + * URL for the quick link + * @visibility frontend + */ + url: string; + /** + * Display label + * @visibility frontend + */ + label: string; + /** + * Icon identifier (e.g. 'catalog') + * @visibility frontend + */ + icon?: string; + /** + * URL to an icon image + * @visibility frontend + */ + iconUrl?: string; + }>; + }; +} diff --git a/packages/app/package.json b/packages/app/package.json index a5b457e..5ff6e8d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -6,6 +6,7 @@ "backstage": { "role": "frontend" }, + "configSchema": "config.d.ts", "scripts": { "start": "HOST=0.0.0.0 backstage-cli package start", "build": "backstage-cli package build", @@ -80,6 +81,7 @@ ] }, "files": [ - "dist" + "dist", + "config.d.ts" ] } diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index abb34f2..baf17ac 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -11,6 +11,7 @@ import { catalogImportPlugin, } from '@backstage/plugin-catalog-import'; import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { CustomScaffolderPage } from './components/scaffolder/CustomScaffolderPage'; import { orgPlugin } from '@backstage/plugin-org'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage-community/plugin-tech-radar'; @@ -46,7 +47,7 @@ const app = createApp({ themes: [cnoeVibrantLightAppTheme, cnoeVibrantDarkAppTheme], components: { SignInPage: props => { - return ; + return ; }, }, bindRoutes({ bind }) { @@ -88,7 +89,8 @@ const routes = ( - } /> + } /> + } /> } /> { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + }; + if (open) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [open]); + + return ( +
+ + {open && ( +
+
+ Type + +
+
+ Owner + +
+
+ Lifecycle + +
+
+ Tags + +
+
+ )} +
+ ); +}; + +const TemplateGrid = () => { + const { entities, loading } = useEntityList(); + const navigate = useNavigate(); + + if (loading) { + return ( + + Loading templates... + + ); + } + + const templates = entities as Entity[]; + + if (!templates || templates.length === 0) { + return ( + + No templates found matching your filters. + + ); + } + + return ( + + {templates.map(template => { + const name = template.metadata.name; + const namespace = template.metadata.namespace ?? 'default'; + const title = template.metadata.title || name; + const description = template.metadata.description || ''; + const tags = template.metadata.tags || []; + const type = (template.spec as any)?.type || 'service'; + const owner = (template.spec as any)?.owner || 'unknown'; + + return ( + + + + + + + + + + + + } + sx={{ pb: 0 }} + /> + + + {title} + + + {description} + + {tags.length > 0 && ( + + {tags.map(tag => ( + + ))} + + )} + + + + + + {owner} + + + + + + ); + })} + + ); +}; + +export const CustomScaffolderPage = () => { + return ( + +
+ + + + + + ); +}; diff --git a/packages/app/src/components/scaffolder/ScaffolderFilterDropdown.tsx b/packages/app/src/components/scaffolder/ScaffolderFilterDropdown.tsx new file mode 100644 index 0000000..82e6694 --- /dev/null +++ b/packages/app/src/components/scaffolder/ScaffolderFilterDropdown.tsx @@ -0,0 +1,115 @@ +import { useState, useRef, useEffect, type CSSProperties } from 'react'; +import { + EntityLifecyclePicker, + EntityOwnerPicker, + EntityTypePicker, + EntityTagPicker, +} from '@backstage/plugin-catalog-react'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +const filterBtnBase: CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: 6, + padding: '8px 16px', + borderRadius: 20, + border: '1px solid rgba(255,152,0,0.3)', + background: 'transparent', + cursor: 'pointer', + fontSize: 14, + fontWeight: 500, + color: '#FF9800', + transition: 'all 0.2s ease', + position: 'relative' as const, +}; + +const filterBtnActiveStyle: CSSProperties = { + ...filterBtnBase, + background: 'rgba(255,152,0,0.08)', + borderColor: 'rgba(255,152,0,0.5)', + color: '#FF9800', +}; + +const dropdownStyle: CSSProperties = { + position: 'absolute', + top: 'calc(100% + 8px)', + right: 0, + width: 280, + background: 'var(--backstage-color-background-paper, #fff)', + borderRadius: 16, + boxShadow: '0 8px 32px rgba(0,0,0,0.14), 0 2px 8px rgba(0,0,0,0.06)', + border: '1px solid rgba(0,0,0,0.06)', + padding: '16px', + zIndex: 10, + display: 'flex', + flexDirection: 'column', + gap: 12, +}; + +const filterSectionStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 4, +}; + +const filterLabelStyle: CSSProperties = { + fontSize: 11, + fontWeight: 600, + textTransform: 'uppercase', + letterSpacing: '0.5px', + color: 'var(--backstage-color-text-secondary, #666)', + padding: '0 4px', +}; + +export const ScaffolderFilterDropdown = () => { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + }; + if (open) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [open]); + + return ( +
+ + {open && ( +
+
+ Type + +
+
+ Owner + +
+
+ Lifecycle + +
+
+ Tags + +
+
+ )} +
+ ); +}; diff --git a/packages/backend/package.json b/packages/backend/package.json index 35312f0..4f378c2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -59,7 +59,8 @@ "fs-extra": "~11.2.0", "js-yaml": "^4.1.0", "node-gyp": "^10.0.0", - "yaml": "^2.3.0" + "yaml": "^2.3.0", + "@backstage/plugin-scaffolder-backend-module-gitlab": "^0.8.0" }, "devDependencies": { "@backstage/backend-test-utils": "^1.11.1", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9a118a9..1991860 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -28,6 +28,7 @@ backend.add( if (process.env.GITHUB_TOKEN) { backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); } +backend.add(import('@backstage/plugin-scaffolder-backend-module-gitlab')); // CNOE custom scaffolder actions (gitea publish, argocd, k8s-apply, sanitize, verify) backend.add(cnoeScaffolderActions); diff --git a/packages/backend/src/modules/actions/k8s-apply.ts b/packages/backend/src/modules/actions/k8s-apply.ts index 3095e35..ddb3a98 100644 --- a/packages/backend/src/modules/actions/k8s-apply.ts +++ b/packages/backend/src/modules/actions/k8s-apply.ts @@ -34,15 +34,16 @@ interface ConfFile { users: User[]; } -export const createKubernetesApply = (config: Config) => { +export const createKubernetesApply = (config: Config, actionId = 'cnoe:kubernetes:apply') => { return createTemplateAction<{ manifestString?: string; manifestObject?: any; manifestPath?: string; + manifest?: string; namespaced: boolean; clusterName?: string; }>({ - id: 'cnoe:kubernetes:apply', + id: actionId, schema: { input: { type: 'object', @@ -53,6 +54,11 @@ export const createKubernetesApply = (config: Config) => { title: 'Manifest', description: 'The manifest to apply in the cluster. Must be a string', }, + manifest: { + type: 'string', + title: 'Manifest', + description: 'Alias for manifestString', + }, manifestObject: { type: 'object', title: 'Manifest', @@ -86,8 +92,8 @@ export const createKubernetesApply = (config: Config) => { ctx.workspacePath, 'to-be-applied.yaml', ); - if (ctx.input.manifestString) { - fs.writeFileSync(manifestPath, ctx.input.manifestString, { + if (ctx.input.manifestString || ctx.input.manifest) { + fs.writeFileSync(manifestPath, ctx.input.manifestString || ctx.input.manifest!, { encoding: 'utf8', mode: '600', }); @@ -194,7 +200,32 @@ export const createKubernetesApply = (config: Config) => { } return; } - throw new Error('please specify a valid cluster name'); + // No clusterName specified — use in-cluster service account + let counter = 1; + for (const obj of objList) { + const manifestFilePath = resolveSafeChildPath( + ctx.workspacePath, + `to-be-applied-${counter}.yaml`, + ); + fs.writeFileSync(manifestFilePath, yaml.dump(obj), { + encoding: 'utf8', + mode: '600', + }); + if (obj.metadata.generateName !== undefined) { + await executeShellCommand({ + command: 'kubectl', + args: ['create', '-f', manifestFilePath], + logStream: ctx.logStream, + }); + } else { + await executeShellCommand({ + command: 'kubectl', + args: ['apply', '-f', manifestFilePath], + logStream: ctx.logStream, + }); + } + counter += 1; + } }, }); }; diff --git a/packages/backend/src/modules/scaffolder.ts b/packages/backend/src/modules/scaffolder.ts index cd83d25..a99b363 100644 --- a/packages/backend/src/modules/scaffolder.ts +++ b/packages/backend/src/modules/scaffolder.ts @@ -27,6 +27,7 @@ export const cnoeScaffolderActions = createBackendModule({ createPublishGiteaAction({ integrations, config }), createArgoCDApp({ config, logger }), createKubernetesApply(config), + createKubernetesApply(config, 'kube:apply'), createSanitizeResource(), createVerifyDependency(), );