diff --git a/.changeset/eight-cougars-collect-due.md b/.changeset/eight-cougars-collect-due.md new file mode 100644 index 0000000000..ff997f876a --- /dev/null +++ b/.changeset/eight-cougars-collect-due.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes-react': patch +--- + +Improved rendering of Kubernetes resources' metadata. diff --git a/.changeset/eight-cougars-collect.md b/.changeset/eight-cougars-collect.md new file mode 100644 index 0000000000..edaa18d826 --- /dev/null +++ b/.changeset/eight-cougars-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added `nestedValuesAsYaml` option to `StructuredMetadataTable` to render data as yaml. diff --git a/.changeset/fair-rocks-dream.md b/.changeset/fair-rocks-dream.md new file mode 100644 index 0000000000..d1f7f3f0f7 --- /dev/null +++ b/.changeset/fair-rocks-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed an issue where invalid expressions or non-object values in `step.each` caused an error. diff --git a/.changeset/fifty-turtles-count.md b/.changeset/fifty-turtles-count.md new file mode 100644 index 0000000000..76bea0dcaa --- /dev/null +++ b/.changeset/fifty-turtles-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Add missing route ref to the `/alpha` entity content extension. diff --git a/.changeset/grumpy-crews-sneeze.md b/.changeset/grumpy-crews-sneeze.md new file mode 100644 index 0000000000..92aeff14a8 --- /dev/null +++ b/.changeset/grumpy-crews-sneeze.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +The log message written when plugins fail to initialize now includes the error that caused the plugin startup to fail. diff --git a/.changeset/loud-baboons-relate.md b/.changeset/loud-baboons-relate.md new file mode 100644 index 0000000000..8f719861e1 --- /dev/null +++ b/.changeset/loud-baboons-relate.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-scaffolder': minor +--- + +Updating the `TaskLogStream` to take up all space in a running task, and also show the last line of the log by default diff --git a/.changeset/neat-singers-rhyme.md b/.changeset/neat-singers-rhyme.md new file mode 100644 index 0000000000..5c12339d84 --- /dev/null +++ b/.changeset/neat-singers-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed the file path pattern of many static assets output as part of the frontend build process, where there was an extra `.` before the extension, leading to names like `image-af7946b..png`. diff --git a/.changeset/odd-countries-vanish.md b/.changeset/odd-countries-vanish.md new file mode 100644 index 0000000000..4d56b1fdeb --- /dev/null +++ b/.changeset/odd-countries-vanish.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': patch +--- + +Make `ldapOrg` config key optional diff --git a/.changeset/witty-pets-march.md b/.changeset/witty-pets-march.md new file mode 100644 index 0000000000..163d146027 --- /dev/null +++ b/.changeset/witty-pets-march.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Allow route references to be installed in multiple app instances as long as their name is the same. diff --git a/canon-docs/src/app/(docs)/components/button/page.mdx b/canon-docs/src/app/(docs)/components/button/page.mdx index c35c4888f3..e0b0a89f31 100644 --- a/canon-docs/src/app/(docs)/components/button/page.mdx +++ b/canon-docs/src/app/(docs)/components/button/page.mdx @@ -167,9 +167,7 @@ Here's a view when buttons are responsive. py={4} open preview={} - code={``} + code={``} /> diff --git a/canon-docs/src/app/(docs)/components/heading/page.mdx b/canon-docs/src/app/(docs)/components/heading/page.mdx index 4e3b0aa46c..e843e131ce 100644 --- a/canon-docs/src/app/(docs)/components/heading/page.mdx +++ b/canon-docs/src/app/(docs)/components/heading/page.mdx @@ -93,5 +93,7 @@ on the screen size. py={4} open preview={} - code={`Responsive`} + code={` + Responsive heading + `} /> diff --git a/canon-docs/src/app/(docs)/components/table/page.mdx b/canon-docs/src/app/(docs)/components/table/page.mdx index f91505acf6..f169bcd7f9 100644 --- a/canon-docs/src/app/(docs)/components/table/page.mdx +++ b/canon-docs/src/app/(docs)/components/table/page.mdx @@ -1 +1 @@ -# Table +# Table (Coming soon) diff --git a/canon-docs/src/app/(docs)/components/text/page.mdx b/canon-docs/src/app/(docs)/components/text/page.mdx index 79f9f96a6b..6d96fc5da3 100644 --- a/canon-docs/src/app/(docs)/components/text/page.mdx +++ b/canon-docs/src/app/(docs)/components/text/page.mdx @@ -137,5 +137,7 @@ on the screen size. } - code={`Responsive`} + code={` + Responsive text + `} /> diff --git a/canon-docs/src/app/(docs)/theme/theming/page.mdx b/canon-docs/src/app/(docs)/theme/theming/page.mdx index 3a16b533e9..4cb03859f9 100644 --- a/canon-docs/src/app/(docs)/theme/theming/page.mdx +++ b/canon-docs/src/app/(docs)/theme/theming/page.mdx @@ -1,67 +1,70 @@ import { CodeBlock } from '@/components/CodeBlock'; import * as Table from '@/components/Table'; import { Chip } from '@/components/Chip'; +import { customTheme } from '@/snippets/_snippets'; # Theming -Backstage ships with a default theme with a light and dark mode variant. The -themes are provided as a part of the `@backstage/canon` package, which also -includes utilities for customizing the default theme, or creating completely -new themes. +Canon's theming is built entirely on CSS, without relying on any CSS-in-JS libraries. +At its core, it provides a solid default theme that is easily customizable using a +comprehensive set of CSS variables. Additionally, it enables anyone to adapt the design +to their specific needs. Each component comes with fixed class names, making customization +even more straightforward. ## Light & Dark modes -By default we are supporting both light and dark modes. Each user can opt to -choose what theme they want to use or to use their system decide what theme to -use. If you want to create your own theme, you will have to set both light and -dark themes following the instructions below. If you only set one of them, the -other mode will fallback to the default theme. +By default, Canon supports both light and dark modes using the `data-theme` attribute. +The light theme is applied by default if no `data-theme` attribute is specified. To create +a custom theme, you'll need to define both light and dark modes as outlined below. If +only one mode is defined, the other will fall back to the default theme. ## How to create your own theme -To create your own theme, you will have to define the variables below. To do -that, create a theme.css file and import it in your application. Here's an -example below on how to set your light and dark mode. +In our [started guide](/), we ask you to import two css files. The `core.css` file includes +the default set of variables. We recommend to keep this file in place and add your own theme +on top of it. `core.css` also include an opinionated reset. If you decided to remove `core.css` +you will have to provide your own reset css. - +Here's an example of how your theme.css file should look like: -## Colors + -We provide a set of generic colours tokens that we use across Canon. By -changing these colours you can easily change the look and feel of your -application to match your brand. +## Available CSS variables + +### Core background colors + +These colors are used for the background of your application. We are mostly using for now a +single elevated background for panels. `--canon-bg` should mostly use as the main background +color of your app. + + + + + Prop + Description + + + + + + --canon-bg + + The background color of your Backstage instance. + + + + --canon-bg-elevated + + Use for any panels or elevated surfaces. + + + + +### Component background colors + +These colors are meants for interactive components like buttons, callout avatar, chips, ... +We have two main level of background you can customise. The accent one is mostly used as a primary option +for interactive components. The tint one is used for secondary components like callouts, avatars, ... @@ -75,54 +78,254 @@ application to match your brand. --canon-bg-accent - The accent color for the theme. + Used mostly for primary interactive components. - --canon-bg + --canon-bg-accent-hover + + + Used for interactive components when they are hovered. - The background color for the theme. - --canon-bg-elevated + --canon-bg-accent-focus + + + Used for interactive components when they are in focus. - The first surface color for the theme. - --canon-bg + --canon-bg-accent-disabled + + + Used for interactive components when they are disabled. - The second surface color for the theme. - --canon-outline + --canon-bg-tint - The outline color for the theme. + Used for secondary interactive components. - --canon-outline-focus + --canon-bg-tint-hover + + + Used for interactive components when they are hovered. - The outline focus color for the theme. - --canon-text-primary + --canon-bg-tint-focus + + + Used for interactive components when they are active. - The primary text color for the theme. - --canon-text-secondary + --canon-bg-tint-disabled + + + Used for interactive components when they are disabled. - The secondary text color for the theme. -## Typography +### Status background colors + +These backgrounds are intended for non-interactive elements to visually convey a specific state. + + + + + Prop + Description + + + + + + --canon-bg-danger + + Used to show errors informations. + + + + --canon-bg-warning + + Used to show warnings informations. + + + + --canon-bg-success + + Used to show success informations. + + + + +### Foreground colors + +Foreground colours are meant to work in pair with a background colours. Typeically this would work +for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors +are prefixed with `fg` to make it easier to identify. + + + + + Prop + Description + + + + + + --canon-fg-text-primary + + + It should be used on top of `--canon-bg-app` or `--canon-bg-elevated`. + + + + + --canon-fg-text-secondary + + + It should be used on top of `--canon-bg-app` or `--canon-bg-elevated`. + + + + + --canon-fg-link + + + It should be used on top of `--canon-bg-app` or `--canon-bg-elevated`. + + + + + --canon-fg-link-hover + + + It should be used on top of `--canon-bg-app` or `--canon-bg-elevated`. + + + + + --canon-fg-accent + + It should be used on top of `--canon-bg-accent`. + + + + --canon-fg-tint + + It should be used on top of `--canon-bg-tint`. + + + + --canon-fg-danger + + It should be used on top of `--canon-bg-danger`. + + + + --canon-fg-warning + + It should be used on top of `--canon-bg-warning`. + + + + --canon-fg-success + + It should be used on top of `--canon-bg-success`. + + + + +### Border colors + +These border colors are mostly meant to be used as borders on top of any components with +low contrast to help as a separator with the different background colors. + + + + + Prop + Description + + + + + + --canon-border + + + It should be used on top of `--canon-bg-elevated`. + + + + + --canon-border-hover + + + Used when the component is interactive and hovered. + + + + + --canon-border-focus + + + Used when the component is interactive and focused. + + + + + --canon-border-tint + + It should be used on top of `--canon-bg-tint`. + + + + --canon-border-tint-hover + + Used when the component is hovered. + + + + --canon-border-tint-focus + + Used when the component is in focus. + + + + --canon-border-danger + + It should be used on top of `--canon-bg-danger`. + + + + --canon-border-warning + + It should be used on top of `--canon-bg-warning`. + + + + --canon-border-success + + It should be used on top of `--canon-bg-success`. + + + + +## Font families We have two fonts that we use across Canon. The first one is the sans-serif font that we use for the body of the application. The second one is the @@ -151,12 +354,9 @@ monospace font that we use for code blocks and tables. -## Spacing +## Font weights -Our default spacing system is made to work in most scenarios. We have 7 scale -values from `xxs` to `xxl`. We use the values on padding and margin in our -layout components mostly. If you prefer to use a different spacing system, you -can do that by changing the values below. +We have two font weights that we use across Canon. Regular or Bold @@ -168,53 +368,184 @@ can do that by changing the values below. - --canon-space-unit - - - The base unit for the spacing system. Default value is `1em` + --canon-font-weight-regular + The regular font weight for the theme. - --canon-space-xxs + --canon-font-weight-bold - Default value is `0.25 x space unit` - - - - --canon-space-xs - - Default value is `0.5 x space unit` - - - - --canon-space-sm - - Default value is `0.75 x space unit` - - - - --canon-space-md - - Default value is `1.25 x space unit` - - - - --canon-space-lg - - Default value is `2 x space unit` - - - - --canon-space-xl - - Default value is `3.25 x space unit` - - - - --canon-space-xxl - - Default value is `5.25 x space unit` + The bold font weight for the theme. + + + + +## Spacing + +We built a spacing system based on a single value `--canon-space`. This value is +used to calculate the spacing for all the components. By default if you would like to +increase or decrease the spacing between your components you can do it simply by updating +`--canon-space` and it will apply to all spacing values. + +`--canon-space` is not used directly in any components but serve as an easy way to +calculate the other values. + + + + + Prop + Description + + + + + + --canon-space + + + The base unit for the spacing system. Default value is `0.25rem`. + + + + + +Below is the list of all spacing values you can use in your application. We use these +tokens for pretty much each spacing properties like padding, margin, gaps, ... + + + + + Prop + Description + + + + + + --canon-space-0_5 + + Base unit (`--canon-space`) times 0.5. + + + + --canon-space-1 + + Base unit (`--canon-space`). + + + + --canon-space-1_5 + + Base unit (`--canon-space`) times 1.5. + + + + --canon-space-2 + + Base unit (`--canon-space`) times 2. + + + + --canon-space-3 + + Base unit (`--canon-space`) times 3. + + + + --canon-space-4 + + Base unit (`--canon-space`) times 4. + + + + --canon-space-5 + + Base unit (`--canon-space`) times 5. + + + + --canon-space-6 + + Base unit (`--canon-space`) times 6. + + + + --canon-space-7 + + Base unit (`--canon-space`) times 7. + + + + --canon-space-8 + + Base unit (`--canon-space`) times 8. + + + + --canon-space-9 + + Base unit (`--canon-space`) times 9. + + + + --canon-space-10 + + Base unit (`--canon-space`) times 10. + + + + --canon-space-11 + + Base unit (`--canon-space`) times 11. + + + + --canon-space-12 + + Base unit (`--canon-space`) times 12. + + + + --canon-space-13 + + Base unit (`--canon-space`) times 13. + + + + --canon-space-14 + + Base unit (`--canon-space`) times 14. + + + + +### Radius (Work in progress) + +Our radius system is a little bit different from spacing as we use a factor to calculate the radius values. +Under the hood different components uses different radius values. For example, button can be set to have full radius +while a checkbox will have to remain a square. + +Here are the different radius factor we recommend to use + +- None: `0` +- Small: `0.75` +- Medium: `1` +- Large: `1.5` + + + + + Prop + Description + + + + + + --canon-radius-factor + + By default the value is set to medium `1`. diff --git a/canon-docs/src/app/globals.css b/canon-docs/src/app/globals.css index 71f7b8be9c..343f23a38f 100644 --- a/canon-docs/src/app/globals.css +++ b/canon-docs/src/app/globals.css @@ -2,7 +2,7 @@ body { display: flex; flex-direction: row; background-color: var(--canon-bg); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); transition: background-color 0.2s ease-in-out; --docs-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, diff --git a/canon-docs/src/app/page.module.css b/canon-docs/src/app/page.module.css index b0fbed71c1..bc28185d48 100644 --- a/canon-docs/src/app/page.module.css +++ b/canon-docs/src/app/page.module.css @@ -17,7 +17,7 @@ } .page p { - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); font-size: 1rem; line-height: 1.5rem; margin-top: 0; diff --git a/canon-docs/src/components/CodeBlock/styles.module.css b/canon-docs/src/components/CodeBlock/styles.module.css index b5f005f962..966b3ff72b 100644 --- a/canon-docs/src/components/CodeBlock/styles.module.css +++ b/canon-docs/src/components/CodeBlock/styles.module.css @@ -18,7 +18,7 @@ border-bottom: 1px solid var(--canon-border); padding: 12px 20px; font-size: 0.875rem; - color: var(--canon-text-secondary); + color: var(--canon-fg-text-secondary); } .code { diff --git a/canon-docs/src/components/CustomTheme/styles.module.css b/canon-docs/src/components/CustomTheme/styles.module.css index fdb2d00fba..ac7d7f36f1 100644 --- a/canon-docs/src/components/CustomTheme/styles.module.css +++ b/canon-docs/src/components/CustomTheme/styles.module.css @@ -66,7 +66,7 @@ padding: 0 8px; color: #fff; background-color: var(--canon-bg); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); transition: background-color 0.2s ease-in-out; border-radius: 0.25rem; cursor: pointer; diff --git a/canon-docs/src/components/HeadlessBanners/styles.module.css b/canon-docs/src/components/HeadlessBanners/styles.module.css index 1353f150bb..4817ab569f 100644 --- a/canon-docs/src/components/HeadlessBanners/styles.module.css +++ b/canon-docs/src/components/HeadlessBanners/styles.module.css @@ -10,7 +10,7 @@ } .icon path { - fill: var(--canon-text-primary); + fill: var(--canon-fg-text-primary); } .content { @@ -37,6 +37,6 @@ border-radius: 100px; margin-top: var(--canon-space-3); font-size: var(--canon-font-size-3); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); gap: var(--canon-space-2); } diff --git a/canon-docs/src/components/Sidebar/Sidebar.module.css b/canon-docs/src/components/Sidebar/Sidebar.module.css index 43f1681812..feb9f8bea3 100644 --- a/canon-docs/src/components/Sidebar/Sidebar.module.css +++ b/canon-docs/src/components/Sidebar/Sidebar.module.css @@ -10,7 +10,7 @@ left: 0; width: 300px; height: 100vh; - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); background-color: var(--canon-bg-elevated); border-right: 1px solid var(--canon-border); padding-left: 20px; @@ -26,7 +26,7 @@ } .logo path { - fill: var(--canon-text-primary); + fill: var(--canon-fg-text-primary); } .menu { @@ -75,11 +75,11 @@ font-family: var(--docs-font); font-size: var(--canon-font-size-3); font-weight: var(--canon-font-weight-regular); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); } .lineStatus { font-family: var(--docs-font); font-size: var(--canon-font-size-3); - color: var(--canon-text-secondary); + color: var(--canon-fg-text-secondary); } diff --git a/canon-docs/src/components/Table/styles.module.css b/canon-docs/src/components/Table/styles.module.css index 7ac48709eb..9b67b25025 100644 --- a/canon-docs/src/components/Table/styles.module.css +++ b/canon-docs/src/components/Table/styles.module.css @@ -38,7 +38,7 @@ transition: background-color 0.2s ease-in-out; & p { - margin: 0; + margin: 0 !important; } } diff --git a/canon-docs/src/components/Tabs/parts.tsx b/canon-docs/src/components/Tabs/parts.tsx index e0ae296136..41dfadf2ea 100644 --- a/canon-docs/src/components/Tabs/parts.tsx +++ b/canon-docs/src/components/Tabs/parts.tsx @@ -34,8 +34,8 @@ export const Tab = (props: React.ComponentProps) => ( {...rest} style={{ color: state.selected - ? 'var(--canon-text-primary)' - : 'var(--canon-text-secondary)', + ? 'var(--canon-fg-text-primary)' + : 'var(--canon-fg-text-secondary)', }} > {children} diff --git a/canon-docs/src/components/Tabs/styles.module.css b/canon-docs/src/components/Tabs/styles.module.css index 8d898fd727..137bc7470d 100644 --- a/canon-docs/src/components/Tabs/styles.module.css +++ b/canon-docs/src/components/Tabs/styles.module.css @@ -22,6 +22,6 @@ left: var(--active-tab-left); width: var(--active-tab-width); height: 1px; - background-color: var(--canon-text-primary); + background-color: var(--canon-fg-text-primary); transition: all 0.2s ease-in-out; } diff --git a/canon-docs/src/components/Toolbar/nav.module.css b/canon-docs/src/components/Toolbar/nav.module.css index 90730b1403..526d7bf918 100644 --- a/canon-docs/src/components/Toolbar/nav.module.css +++ b/canon-docs/src/components/Toolbar/nav.module.css @@ -27,21 +27,21 @@ all: unset; height: 60px; font-family: var(--docs-font); - color: var(--canon-text-secondary); + color: var(--canon-fg-text-secondary); font-size: var(--canon-font-size-3); font-weight: var(--canon-font-weight-bold); cursor: pointer; transition: color 0.2s ease-in-out; &:hover { - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); } &[data-selected] { - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); & p { - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); } } @@ -60,7 +60,7 @@ } .tab p { - color: var(--canon-text-secondary) !important; + color: var(--canon-fg-text-secondary) !important; } .indicator { @@ -72,7 +72,7 @@ width: var(--active-tab-width); height: 1px; border-radius: 0.25rem; - background-color: var(--canon-text-primary); + background-color: var(--canon-fg-text-primary); transition-property: translate, width, background-color; transition-duration: 200ms; transition-timing-function: ease-in-out; diff --git a/canon-docs/src/components/Toolbar/theme.module.css b/canon-docs/src/components/Toolbar/theme.module.css index 564f5be4ca..ac3701541f 100644 --- a/canon-docs/src/components/Toolbar/theme.module.css +++ b/canon-docs/src/components/Toolbar/theme.module.css @@ -29,23 +29,23 @@ outline: 0; background: none; appearance: none; - color: var(--canon-text-secondary); + color: var(--canon-fg-text-secondary); user-select: none; height: 2rem; flex: 1; cursor: pointer; &[data-selected] { - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); & p { - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); } } @media (hover: hover) { &:hover { - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); } } @@ -64,7 +64,7 @@ } .tab p { - color: var(--canon-text-secondary) !important; + color: var(--canon-fg-text-secondary) !important; } .indicator { diff --git a/canon-docs/src/mdx-components.tsx b/canon-docs/src/mdx-components.tsx index 85df3315c4..182eaa03b4 100644 --- a/canon-docs/src/mdx-components.tsx +++ b/canon-docs/src/mdx-components.tsx @@ -62,10 +62,13 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {

), a: ({ children, href }) => ( - + {children as ReactNode} ), + li: ({ children }) => ( +
  • {children as ReactNode}
  • + ), pre: ({ children }) => { const codeContent = React.isValidElement(children) ? (children.props as { children: string }).children @@ -80,7 +83,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { backgroundColor: 'var(--canon-bg-elevated)', padding: '0.2rem 0.375rem', borderRadius: '0.25rem', - color: 'var(--canon-text-secondary)', + color: 'var(--canon-fg-text-secondary)', border: '1px solid var(--canon-border)', fontSize: '0.875rem', }} diff --git a/canon-docs/src/snippets/_snippets.ts b/canon-docs/src/snippets/_snippets.ts index 417ff853b5..eb16cbb66c 100644 --- a/canon-docs/src/snippets/_snippets.ts +++ b/canon-docs/src/snippets/_snippets.ts @@ -1,5 +1,36 @@ // Sometimes codes are not formatted correctly in the docs, so we need to use snippets +export const customTheme = `:root { + --canon-font-regular: system-ui; + --canon-font-weight-regular: 400; + --canon-font-weight-bold: 600; + --canon-bg: #f8f8f8; + --canon-bg-elevated: #fff; + /* ... other CSS variables */ + + /* Add your custom components styles here */ + .canon-Button { + background-color: #000; + color: #fff; + } +} + +[data-theme='dark'] { + --canon-font-regular: system-ui; + --canon-font-weight-regular: 400; + --canon-font-weight-bold: 600; + --canon-bg: #f8f8f8; + --canon-bg-elevated: #fff; + /* ... other CSS variables */ + + /* Add your custom components styles here */ + .canon-Button { + background-color: #000; + color: #fff; + } +} +`; + export const grid = `import { Grid } from '@backstage/canon'; diff --git a/canon-docs/src/snippets/button.tsx b/canon-docs/src/snippets/button.tsx index 59e6bbd486..30ed666a6a 100644 --- a/canon-docs/src/snippets/button.tsx +++ b/canon-docs/src/snippets/button.tsx @@ -62,8 +62,11 @@ export const ButtonDisabled = () => { }; export const ButtonResponsive = () => { - // TODO: Add responsive button - return null; + return ( + + ); }; export const ButtonPlayground = () => { diff --git a/canon-docs/src/snippets/heading.tsx b/canon-docs/src/snippets/heading.tsx index 087ce7ebbd..13eaddff30 100644 --- a/canon-docs/src/snippets/heading.tsx +++ b/canon-docs/src/snippets/heading.tsx @@ -19,10 +19,9 @@ export const HeadingAllVariants = () => { }; export const HeadingResponsive = () => { - return null; return ( - + Responsive heading diff --git a/canon-docs/src/snippets/text.tsx b/canon-docs/src/snippets/text.tsx index 633b3b45d7..d2e2020210 100644 --- a/canon-docs/src/snippets/text.tsx +++ b/canon-docs/src/snippets/text.tsx @@ -57,8 +57,9 @@ export const TextAllWeights = () => { }; export const TextResponsive = () => { - // TODO: Add responsive text - return null; + return ( + Responsive text + ); }; export const TextPlayground = () => { diff --git a/canon-docs/yarn.lock b/canon-docs/yarn.lock index 28810f8911..912da3edc2 100644 --- a/canon-docs/yarn.lock +++ b/canon-docs/yarn.lock @@ -925,11 +925,11 @@ __metadata: linkType: hard "@types/node@npm:^20": - version: 20.17.12 - resolution: "@types/node@npm:20.17.12" + version: 20.17.16 + resolution: "@types/node@npm:20.17.16" dependencies: undici-types: ~6.19.2 - checksum: 0c0dbeb4e1480a23071ec38e97bb3d776e0ae9828174bf45f9edcf277caa955945cb10d31d84f7a73c66aaf92ae2e022be6331bd00a4bed1f2ad9639a411d17e + checksum: dd19c04c592b602f20448187b4b6ab0293f22173a8bbe53aa3b559188f078b5261d748a107b84ee0580e2074cda760049b13d6e210f9850039bead01cb666a3f languageName: node linkType: hard @@ -5368,22 +5368,22 @@ __metadata: linkType: hard "typescript@npm:^5": - version: 5.7.2 - resolution: "typescript@npm:5.7.2" + version: 5.7.3 + resolution: "typescript@npm:5.7.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: b55300c4cefee8ee380d14fa9359ccb41ff8b54c719f6bc49b424899d662a5ce62ece390ce769568c7f4d14af844085255e63788740084444eb12ef423b13433 + checksum: 6c38b1e989918e576f0307e6ee013522ea480dfce5f3ca85c9b2d8adb1edeffd37f4f30cd68de0c38a44563d12ba922bdb7e36aa2dac9c51de5d561e6e9a2e9c languageName: node linkType: hard "typescript@patch:typescript@^5#~builtin": - version: 5.7.2 - resolution: "typescript@patch:typescript@npm%3A5.7.2#~builtin::version=5.7.2&hash=5adc0c" + version: 5.7.3 + resolution: "typescript@patch:typescript@npm%3A5.7.3#~builtin::version=5.7.3&hash=5adc0c" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 803430c6da2ba73c25a21880d8d4f08a56d9d2444e6db2ea949ac4abceeece8e4a442b7b9b585db7d8a0b47ebda2060e45fe8ee8b8aca23e27ec1d4844987ee6 + checksum: 633cd749d6cd7bc842c6b6245847173bba99742a60776fae3c0fbcc0d1733cd51a733995e5f4dadd8afb0e64e57d3c7dbbeae953a072ee303940eca69e22f311 languageName: node linkType: hard diff --git a/docs/tooling/cli/02-build-system.md b/docs/tooling/cli/02-build-system.md index 8f9b086c33..f3b29e979d 100644 --- a/docs/tooling/cli/02-build-system.md +++ b/docs/tooling/cli/02-build-system.md @@ -515,6 +515,7 @@ The Backstage tooling supports [ECMAScript modules (ESM)](https://nodejs.org/doc - To enable support for native ESM in tests, you need to run the tests with the `--experimental-vm-modules` flag enabled, typically via `NODE_OPTIONS='--experimental-vm-modules'`. - Declaring a package as `"type": "module"` in `package.json` is supported, but in tests it will cause all local transitive dependencies to also be treated as ESM, regardless of whether they declare `"type": "module"` or not. +- When running tests with coverage enabled the default `babel` coverage provider can mess with the hoisting of named exports. This can be worked around by using the `v8` provider instead by setting `"coverageProvider": "v8"` in the Jest configuration, although note that the `v8` provider is a fair bit slower than the `babel` one. - Node.js has an [ESM interoperability layer with CommonJS](https://nodejs.org/docs/latest-v22.x/api/esm.html#interoperability-with-commonjs) that allows for imports from ESM to identify named exports in CommonJS packages. This interoperability layer is **only** enabled when importing packages with a `.cts` or `.cjs` extension. This is because the interoperability layer is not fully compatible with the NPM ecosystem, and would break package if it was enabled for `.js` files. - Dynamic imports of CommonJS packages will vary in shape depending on the runtime, i.e. test vs local development, etc. It is therefore recommended to avoid dynamic imports of CommonJS packages and instead use `require`, or to use the explicit CommonJS extensions as mentioned above. If you do need to dynamically import CommonJS packages, avoid using `default` exports, as the shape of them vary across different environments and you would otherwise need to manually unwrap the import based on the shape of the module object. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index aa48c26877..6bec5439ec 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -34,7 +34,7 @@ import type { } from '../../../backend-plugin-api/src/wiring/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types'; -import { ForwardedError, ConflictError } from '@backstage/errors'; +import { ForwardedError, ConflictError, assertError } from '@backstage/errors'; import { instanceMetadataServiceRef, featureDiscoveryServiceRef, @@ -405,8 +405,9 @@ export class BackendInitializer { // Once the plugin and all modules have been initialized, we can signal that the plugin has stared up successfully const lifecycleService = await this.#getPluginLifecycleImpl(pluginId); await lifecycleService.startup(); - } catch (error) { - initLogger.onPluginFailed(pluginId); + } catch (error: unknown) { + assertError(error); + initLogger.onPluginFailed(pluginId, error); throw error; } }), diff --git a/packages/backend-app-api/src/wiring/createInitializationLogger.ts b/packages/backend-app-api/src/wiring/createInitializationLogger.ts index 2a472dec5b..e623d43074 100644 --- a/packages/backend-app-api/src/wiring/createInitializationLogger.ts +++ b/packages/backend-app-api/src/wiring/createInitializationLogger.ts @@ -27,7 +27,7 @@ export function createInitializationLogger( rootLogger?: RootLoggerService, ): { onPluginStarted(pluginId: string): void; - onPluginFailed(pluginId: string): void; + onPluginFailed(pluginId: string, error: Error): void; onAllStarted(): void; } { const logger = rootLogger?.child({ type: 'initialization' }); @@ -68,14 +68,15 @@ export function createInitializationLogger( starting.delete(pluginId); started.add(pluginId); }, - onPluginFailed(pluginId: string) { + onPluginFailed(pluginId: string, error: Error) { starting.delete(pluginId); const status = starting.size > 0 ? `, waiting for ${starting.size} other plugins to finish before shutting down the process` : ''; logger?.error( - `Plugin '${pluginId}' threw an error during startup${status}`, + `Plugin '${pluginId}' threw an error during startup${status}.`, + error, ); }, onAllStarted() { diff --git a/packages/canon/.storybook/themes/backstage.css b/packages/canon/.storybook/themes/backstage.css index 92884fa4e7..653836c558 100644 --- a/packages/canon/.storybook/themes/backstage.css +++ b/packages/canon/.storybook/themes/backstage.css @@ -60,9 +60,9 @@ --canon-bg: #f4f4f4; /* Text colors */ - --canon-text-primary: #000; + --canon-fg-text-primary: #000; --canon-fg-accent: #fff; - --canon-text-secondary: #646464; + --canon-fg-text-secondary: #646464; } [data-theme-name='legacy'][data-theme='dark'] { @@ -82,7 +82,7 @@ --canon-fg-danger: #f50000; /* Text colors */ - --canon-text-primary: #fff; + --canon-fg-text-primary: #fff; --canon-fg-accent: #000; - --canon-text-secondary: #b3b3b3; + --canon-fg-text-secondary: #b3b3b3; } diff --git a/packages/canon/src/components/Box/Box.stories.tsx b/packages/canon/src/components/Box/Box.stories.tsx index bf4b8ae5de..52dd7bc782 100644 --- a/packages/canon/src/components/Box/Box.stories.tsx +++ b/packages/canon/src/components/Box/Box.stories.tsx @@ -383,7 +383,7 @@ export const Border: Story = { args: { style: { background: 'var(--canon-bg-elevated)', - color: 'var(--canon-text-primary)', + color: 'var(--canon-fg-text-primary)', padding: '4px 8px', width: '80px', height: '32px', diff --git a/packages/canon/src/components/Box/styles.css b/packages/canon/src/components/Box/styles.css index c36afdbdfc..229f322343 100644 --- a/packages/canon/src/components/Box/styles.css +++ b/packages/canon/src/components/Box/styles.css @@ -1,4 +1,4 @@ .canon-Box { font-family: var(--canon-font-regular); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); } diff --git a/packages/canon/src/components/Button/styles.css b/packages/canon/src/components/Button/styles.css index 3456f8aa1a..6bfd93a8ea 100644 --- a/packages/canon/src/components/Button/styles.css +++ b/packages/canon/src/components/Button/styles.css @@ -36,14 +36,14 @@ &:hover { background-color: transparent; box-shadow: inset 0 0 0 1px var(--canon-border-focus); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); } } .canon-Button--variant-secondary { background-color: transparent; box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); &:hover { box-shadow: inset 0 0 0 1px var(--canon-border-hover); @@ -52,10 +52,10 @@ .canon-Button--variant-tertiary { background-color: transparent; - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); &:hover { - color: var(--canon-text-secondary); + color: var(--canon-fg-text-secondary); } } diff --git a/packages/canon/src/components/Checkbox/styles.css b/packages/canon/src/components/Checkbox/styles.css index 76e14029ca..1da96e8344 100644 --- a/packages/canon/src/components/Checkbox/styles.css +++ b/packages/canon/src/components/Checkbox/styles.css @@ -23,7 +23,7 @@ gap: var(--canon-space-2); font-size: var(--canon-font-size-xs); font-family: var(--canon-font-regular); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); user-select: none; &:hover { diff --git a/packages/canon/src/components/Field/Field.styles.css b/packages/canon/src/components/Field/Field.styles.css index 317dce8143..6bf695fc97 100644 --- a/packages/canon/src/components/Field/Field.styles.css +++ b/packages/canon/src/components/Field/Field.styles.css @@ -24,14 +24,14 @@ .canon-FieldLabel { font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); margin-bottom: var(--canon-space-1_5); } .canon-FieldDescription { font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); - color: var(--canon-text-secondary); + color: var(--canon-fg-text-secondary); margin: 0; padding-top: var(--canon-space-1_5); } @@ -47,7 +47,7 @@ .canon-FieldValidity { font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); - color: var(--canon-text-secondary); + color: var(--canon-fg-text-secondary); margin: 0; padding-top: var(--canon-space-1_5); } diff --git a/packages/canon/src/components/Heading/styles.css b/packages/canon/src/components/Heading/styles.css index bc121c1063..a730afe9f0 100644 --- a/packages/canon/src/components/Heading/styles.css +++ b/packages/canon/src/components/Heading/styles.css @@ -16,7 +16,7 @@ .canon-Heading { font-family: var(--canon-font-regular); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); line-height: 100%; padding: 0; margin: 0; diff --git a/packages/canon/src/components/Input/Input.styles.css b/packages/canon/src/components/Input/Input.styles.css index 51ad1c5afe..cfeed27b48 100644 --- a/packages/canon/src/components/Input/Input.styles.css +++ b/packages/canon/src/components/Input/Input.styles.css @@ -21,13 +21,13 @@ background-color: var(--canon-bg-elevated); font-size: var(--canon-font-size-3); font-weight: var(--canon-font-weight-regular); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; width: 100%; } .canon-Input::placeholder { - color: var(--canon-text-secondary); + color: var(--canon-fg-text-secondary); } .canon-Input:hover { diff --git a/packages/canon/src/components/Text/styles.css b/packages/canon/src/components/Text/styles.css index 62f9d1eec0..9cf8c884fb 100644 --- a/packages/canon/src/components/Text/styles.css +++ b/packages/canon/src/components/Text/styles.css @@ -16,7 +16,7 @@ .canon-Text { font-family: var(--canon-font-regular); - color: var(--canon-text-primary); + color: var(--canon-fg-text-primary); padding: 0; margin: 0; } diff --git a/packages/canon/src/css/core.css b/packages/canon/src/css/core.css index bd03d4d906..d744d468d0 100644 --- a/packages/canon/src/css/core.css +++ b/packages/canon/src/css/core.css @@ -75,7 +75,7 @@ --canon-bg: #f8f8f8; --canon-bg-elevated: #fff; --canon-bg-accent: #000; - --canon-bg-accent-active: #000; + --canon-bg-accent-focus: #000; --canon-bg-accent-disabled: #000; --canon-bg-accent-hover: #000; --canon-bg-tint: #edf2fe; @@ -95,14 +95,15 @@ --canon-border-hover: rgba(0, 0, 0, 0.2); --canon-border-success: #008000; --canon-border-warning: #e36d05; + --canon-fg-text-primary: #000; + --canon-fg-text-secondary: #646464; + --canon-fg-link: #fff; + --canon-fg-link-hover: #fff; --canon-fg-accent: #fff; + --canon-fg-tint: #000; --canon-fg-danger: #e22b2b; - --canon-fg-success: #008000; --canon-fg-warning: #e36d05; - --canon-text-link: #fff; - --canon-text-link-hover: #fff; - --canon-text-primary: #000; - --canon-text-secondary: #646464; + --canon-fg-success: #008000; } /* Dark theme tokens */ @@ -110,7 +111,7 @@ --canon-bg: #f8f8f8; --canon-bg-elevated: #fff; --canon-bg-accent: #000; - --canon-bg-accent-active: #000; + --canon-bg-accent-focus: #000; --canon-bg-accent-disabled: #000; --canon-bg-accent-hover: #000; --canon-bg-tint: #edf2fe; @@ -134,8 +135,8 @@ --canon-fg-danger: #e22b2b; --canon-fg-success: #008000; --canon-fg-warning: #e36d05; - --canon-text-link: #fff; - --canon-text-link-hover: #fff; - --canon-text-primary: #000; - --canon-text-secondary: #646464; + --canon-fg-link: #fff; + --canon-fg-link-hover: #fff; + --canon-fg-text-primary: #000; + --canon-fg-text-secondary: #646464; } diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index addefacd6e..f0a8a9d01d 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -154,7 +154,7 @@ export const transforms = (options: TransformOptions): Transforms => { ], type: 'asset/resource', generator: { - filename: 'static/[name].[hash:8].[ext]', + filename: 'static/[name].[hash:8][ext]', }, }, { diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 13d3785c4d..add79c01f5 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -73,6 +73,7 @@ "d3-shape": "^3.0.0", "d3-zoom": "^3.0.0", "dagre": "^0.8.5", + "js-yaml": "^4.1.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", "lodash": "^4.17.21", diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index c3f7cf9a6a..c706eef2bb 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -1307,6 +1307,7 @@ export interface StructuredMetadataTableProps { // (undocumented) options?: { titleFormat?: (key: string) => string; + nestedValuesAsYaml?: boolean; }; } diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index cbeb249815..37768ad80b 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -18,12 +18,12 @@ import Box from '@material-ui/core/Box'; import IconButton from '@material-ui/core/IconButton'; import CopyIcon from '@material-ui/icons/FileCopy'; import classnames from 'classnames'; -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import { AnsiProcessor } from './AnsiProcessor'; +import { AnsiLine, AnsiProcessor } from './AnsiProcessor'; import { LogLine } from './LogLine'; import { LogViewerControls } from './LogViewerControls'; import { HEADER_SIZE, useStyles } from './styles'; @@ -37,7 +37,9 @@ export interface RealLogViewerProps { export function RealLogViewer(props: RealLogViewerProps) { const classes = useStyles({ classes: props.classes }); - const listRef = useRef(null); + const [fixedListInstance, setFixedListInstance] = useState | null>(null); // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); @@ -48,10 +50,21 @@ export function RealLogViewer(props: RealLogViewerProps) { const location = useLocation(); useEffect(() => { - if (search.resultLine !== undefined && listRef.current) { - listRef.current.scrollToItem(search.resultLine - 1, 'center'); + if (fixedListInstance) { + fixedListInstance.scrollToItem(lines.length - 1, 'end'); } - }, [search.resultLine]); + }, [fixedListInstance, lines]); + + useEffect(() => { + if (!fixedListInstance) { + return; + } + if (search.resultLine) { + fixedListInstance.scrollToItem(search.resultLine - 1, 'center'); + } else { + fixedListInstance.scrollToItem(lines.length - 1, 'end'); + } + }, [fixedListInstance, search.resultLine, lines]); useEffect(() => { if (location.hash) { @@ -76,7 +89,9 @@ export function RealLogViewer(props: RealLogViewerProps) { ) => { + setFixedListInstance(instance); + }} className={classes.log} height={(height || 480) - HEADER_SIZE} width={width || 640} diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx index 4eb05e715e..8e6c4bbff0 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx @@ -66,7 +66,7 @@ describe('', () => { expect(getByText(startCase(value))).toBeInTheDocument(); }); metadata.arrayField.forEach(value => { - expect(getByText(value)).toBeInTheDocument(); + expect(getByText(new RegExp(value))).toBeInTheDocument(); }); }); @@ -124,12 +124,19 @@ describe('', () => { }; it('should make keys human readable', async () => { - const rendered = render(); + const rendered = render( + , + ); expect(rendered.queryByText(/^Test A/)).toBeInTheDocument(); expect(rendered.queryByText(/^Test B/)).toBeInTheDocument(); - expect(rendered.queryByText(/^Test C/)).toBeInTheDocument(); expect(rendered.queryByText(/^Test D/)).toBeInTheDocument(); - expect(rendered.queryByText(/^Test E/)).toBeInTheDocument(); + + // nested content is displayed as yaml, so not affected by formatting + expect(rendered.queryByText(/^testC/)).toBeInTheDocument(); + expect(rendered.queryByText(/testE: stuff/)).toBeInTheDocument(); }); it('should be possible to disable it', async () => { @@ -161,14 +168,16 @@ describe('', () => { const rendered = render( , ); expect(rendered.queryByText(/^tEsTa/)).toBeInTheDocument(); expect(rendered.queryByText(/^tEsTb/)).toBeInTheDocument(); - expect(rendered.queryByText(/^tEsTc/)).toBeInTheDocument(); expect(rendered.queryByText(/^tEsTd/)).toBeInTheDocument(); - expect(rendered.queryByText(/^tEsTe/)).toBeInTheDocument(); + + // nested content is displayed as yaml, so not affected by formatting + expect(rendered.queryByText(/^testC/)).toBeInTheDocument(); + expect(rendered.queryByText(/^testE/)).toBeInTheDocument(); }); }); }); diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx index 3099edf9d8..b839280cdd 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx @@ -15,21 +15,23 @@ */ import React, { Fragment, ReactElement } from 'react'; -import { - withStyles, - createStyles, - WithStyles, - Theme, -} from '@material-ui/core/styles'; import startCase from 'lodash/startCase'; import Typography from '@material-ui/core/Typography'; import { - MetadataTable, - MetadataTableItem, MetadataList, MetadataListItem, + MetadataTable, + MetadataTableItem, } from './MetadataTable'; +import { CodeSnippet } from '../CodeSnippet'; +import jsyaml from 'js-yaml'; +import { + Theme, + createStyles, + WithStyles, + withStyles, +} from '@material-ui/core/styles'; export type StructuredMetadataTableListClassKey = 'root'; @@ -41,7 +43,6 @@ const listStyle = createStyles({ }); export type StructuredMetadataTableNestedListClassKey = 'root'; - const nestedListStyle = (theme: Theme) => createStyles({ root: { @@ -110,9 +111,24 @@ function toValue( if (React.isValidElement(value)) { return {value}; } - - if (value !== null && typeof value === 'object' && !Array.isArray(value)) { - return renderMap(value, options, nested); + if (value !== null && typeof value === 'object') { + if (options.nestedValuesAsYaml) { + return ( + + ); + } + if (!Array.isArray(value)) { + return renderMap(value, options, nested); + } } if (Array.isArray(value)) { @@ -122,7 +138,6 @@ function toValue( if (typeof value === 'boolean') { return {value ? '✅' : '❌'}; } - return ( {value} @@ -167,6 +182,7 @@ export interface StructuredMetadataTableProps { * @returns Formatted key */ titleFormat?: (key: string) => string; + nestedValuesAsYaml?: boolean; }; } @@ -174,9 +190,10 @@ type Options = Required>; /** @public */ export function StructuredMetadataTable(props: StructuredMetadataTableProps) { - const { metadata, dense = true, options = {} } = props; + const { metadata, dense = true, options } = props; const metadataItems = mapToItems(metadata, { titleFormat: startCase, + nestedValuesAsYaml: options?.nestedValuesAsYaml ?? false, ...options, }); return {metadataItems}; diff --git a/packages/create-app/seed-yarn.lock b/packages/create-app/seed-yarn.lock index 21b2aa543d..ebb1fb1e0b 100644 --- a/packages/create-app/seed-yarn.lock +++ b/packages/create-app/seed-yarn.lock @@ -41,3 +41,18 @@ version "22.2.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== + +"@types/node@*": + version "22.10.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.8.tgz#e7e2602c83d27d483c056302d76b86321c4e8697" + integrity sha512-rk+QvAEGsbX/ZPiiyel6hJHNUS9cnSbPWVaZLvE+Er3tLqQFzWMz9JOfWW7XUmKvRPfxJfbl3qYWve+RGXncFw== + +"@types/node@>=13.7.0": + version "22.10.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.8.tgz#e7e2602c83d27d483c056302d76b86321c4e8697" + integrity sha512-rk+QvAEGsbX/ZPiiyel6hJHNUS9cnSbPWVaZLvE+Er3tLqQFzWMz9JOfWW7XUmKvRPfxJfbl3qYWve+RGXncFw== + +"@types/node@^22.0.0": + version "22.10.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.8.tgz#e7e2602c83d27d483c056302d76b86321c4e8697" + integrity sha512-rk+QvAEGsbX/ZPiiyel6hJHNUS9cnSbPWVaZLvE+Er3tLqQFzWMz9JOfWW7XUmKvRPfxJfbl3qYWve+RGXncFw== diff --git a/packages/frontend-plugin-api/src/routing/RouteRef.test.ts b/packages/frontend-plugin-api/src/routing/RouteRef.test.ts index de7a3de887..570723df9a 100644 --- a/packages/frontend-plugin-api/src/routing/RouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/RouteRef.test.ts @@ -35,6 +35,7 @@ describe('RouteRef', () => { internal.setId('some-id'); expect(String(internal)).toBe('RouteRef{some-id}'); + internal.setId('some-id'); // Should allow same ID expect(() => internal.setId('some-other-id')).toThrow( "RouteRef was referenced twice as both 'some-id' and 'some-other-id'", diff --git a/packages/frontend-plugin-api/src/routing/RouteRef.ts b/packages/frontend-plugin-api/src/routing/RouteRef.ts index da562e5273..1678e14cd4 100644 --- a/packages/frontend-plugin-api/src/routing/RouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/RouteRef.ts @@ -95,7 +95,7 @@ export class RouteRefImpl implements InternalRouteRef { if (!id) { throw new Error(`${this.#name} id must be a non-empty string`); } - if (this.#id) { + if (this.#id && this.#id !== id) { throw new Error( `${this.#name} was referenced twice as both '${this.#id}' and '${id}'`, ); diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts index 73d26924be..20de746b25 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts @@ -82,7 +82,12 @@ describe('generateOpenApiSchema', () => { }), } as any); - const { command } = await import('./index'); + // Same logic as https://github.com/backstage/backstage/blob/547e41da5ac497e5a606c08c8fb57b429687d5f7/packages/repo-tools/src/commands/index.ts#L278-L297 + const { + default: { command }, + } = (await import('./index')) as unknown as { + default: typeof import('./index'); + }; const actions = async () => { while (!mockOn.ready) { diff --git a/plugins/catalog-backend-module-ldap/config.d.ts b/plugins/catalog-backend-module-ldap/config.d.ts index e30aaab5d5..b04e9194d2 100644 --- a/plugins/catalog-backend-module-ldap/config.d.ts +++ b/plugins/catalog-backend-module-ldap/config.d.ts @@ -437,7 +437,7 @@ export interface Config { /** * LdapOrg provider key */ - ldapOrg: { + ldapOrg?: { /** * Id of the LdapOrg provider */ diff --git a/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx b/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx index b77b2dff88..af741239e5 100644 --- a/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx +++ b/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx @@ -58,6 +58,7 @@ export const ClusterOverview = () => { 'OIDC Token Provider': value.oidcTokenProvider ?? 'N/A', 'Dashboard Link': value.dashboardUrl ?? 'N/A', }} + options={{ nestedValuesAsYaml: true }} /> )} diff --git a/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx b/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx index 0b3f7ddb60..9307b11a6e 100644 --- a/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx +++ b/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx @@ -50,7 +50,10 @@ const defaultColumns: TableColumn[] = [ Node Info - + Addresses @@ -61,6 +64,7 @@ const defaultColumns: TableColumn[] = [ return accum; }, {} as any) ?? {} } + options={{ nestedValuesAsYaml: true }} /> @@ -72,6 +76,7 @@ const defaultColumns: TableColumn[] = [ return accum; }, {} as any) ?? {} } + options={{ nestedValuesAsYaml: true }} /> diff --git a/plugins/kubernetes-react/src/components/CustomResources/DefaultCustomResource.tsx b/plugins/kubernetes-react/src/components/CustomResources/DefaultCustomResource.tsx index ee484ecda8..d34ba23f7b 100644 --- a/plugins/kubernetes-react/src/components/CustomResources/DefaultCustomResource.tsx +++ b/plugins/kubernetes-react/src/components/CustomResources/DefaultCustomResource.tsx @@ -84,7 +84,10 @@ const DefaultCustomResourceAccordion = ({ {Object.prototype.hasOwnProperty.call(customResource, 'status') && ( - + )} diff --git a/plugins/kubernetes-react/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx b/plugins/kubernetes-react/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx index 1e8515f81a..002c4c8ff4 100644 --- a/plugins/kubernetes-react/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx @@ -40,13 +40,13 @@ describe('DeploymentDrawer', () => { expect(getAllByText('Deployment')).toHaveLength(2); expect(getByText('YAML')).toBeInTheDocument(); expect(getByText('Strategy')).toBeInTheDocument(); - expect(getByText('Rolling Update:')).toBeInTheDocument(); - expect(getByText(textContentMatcher('Max Surge: 25%'))).toBeInTheDocument(); + expect(getByText('rollingUpdate:')).toBeInTheDocument(); + expect(getByText(textContentMatcher('maxSurge: 25%'))).toBeInTheDocument(); expect( - getByText(textContentMatcher('Max Unavailable: 25%')), + getByText(textContentMatcher('maxUnavailable: 25%')), ).toBeInTheDocument(); expect( - getByText(textContentMatcher('Type: RollingUpdate')), + getByText(textContentMatcher('type: RollingUpdate')), ).toBeInTheDocument(); expect(getByText('Min Ready Seconds')).toBeInTheDocument(); expect(getByText('???')).toBeInTheDocument(); diff --git a/plugins/kubernetes-react/src/components/IngressesAccordions/IngressDrawer.test.tsx b/plugins/kubernetes-react/src/components/IngressesAccordions/IngressDrawer.test.tsx index cfacb24ee4..e504aefbb9 100644 --- a/plugins/kubernetes-react/src/components/IngressesAccordions/IngressDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/IngressesAccordions/IngressDrawer.test.tsx @@ -37,13 +37,13 @@ describe('IngressDrawer', () => { expect(screen.getByText('YAML')).toBeInTheDocument(); expect(screen.getByText('Rules')).toBeInTheDocument(); expect( - screen.getByText(textContentMatcher('Host: api.awesome-host.io')), + screen.getByText(textContentMatcher('host: api.awesome-host.io')), ).toBeInTheDocument(); expect( - screen.getAllByText(textContentMatcher('Service Port: 80')), + screen.getAllByText(textContentMatcher('servicePort: 80')), ).toHaveLength(2); expect( - screen.getAllByText(textContentMatcher('Service Name: awesome-service')), + screen.getAllByText(textContentMatcher('serviceName: awesome-service')), ).toHaveLength(2); }); }); diff --git a/plugins/kubernetes-react/src/components/IngressesAccordions/IngressesAccordions.tsx b/plugins/kubernetes-react/src/components/IngressesAccordions/IngressesAccordions.tsx index 5962e78f59..d2d674042b 100644 --- a/plugins/kubernetes-react/src/components/IngressesAccordions/IngressesAccordions.tsx +++ b/plugins/kubernetes-react/src/components/IngressesAccordions/IngressesAccordions.tsx @@ -65,6 +65,9 @@ const IngressCard = ({ ingress }: IngressCardProps) => { metadata={{ ...ingress.spec, }} + options={{ + nestedValuesAsYaml: true, + }} /> ); }; diff --git a/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx b/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx index b31d3f9aa7..0a5802ddc8 100644 --- a/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx +++ b/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx @@ -237,6 +237,7 @@ const KubernetesStructuredMetadataTableDrawerContent = < {!isYaml && ( )} diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx index 1919bc112d..6aed2c75d7 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx @@ -176,6 +176,7 @@ export const ContainerCard: React.FC = ({ containerSpec, containerStatus, )} + options={{ nestedValuesAsYaml: true }} /> {containerMetrics && ( diff --git a/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.test.tsx b/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.test.tsx index 7eec2926c4..692c0a7c8c 100644 --- a/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.test.tsx @@ -39,10 +39,10 @@ describe('ServiceDrawer', () => { expect(screen.getByText('Cluster IP')).toBeInTheDocument(); expect(screen.getByText('Ports')).toBeInTheDocument(); expect( - screen.getByText(textContentMatcher('Target Port: 1997')), + screen.getByText(textContentMatcher('targetPort: 1997')), ).toBeInTheDocument(); expect( - screen.getByText(textContentMatcher('App: awesome-service')), + screen.getByText(textContentMatcher('app: awesome-service')), ).toBeInTheDocument(); }); }); diff --git a/plugins/kubernetes-react/src/components/ServicesAccordions/ServicesAccordions.tsx b/plugins/kubernetes-react/src/components/ServicesAccordions/ServicesAccordions.tsx index 71832fd35c..c44a1d158c 100644 --- a/plugins/kubernetes-react/src/components/ServicesAccordions/ServicesAccordions.tsx +++ b/plugins/kubernetes-react/src/components/ServicesAccordions/ServicesAccordions.tsx @@ -77,6 +77,7 @@ const ServiceCard = ({ service }: ServiceCardProps) => { ports: service.spec?.ports, ...metadata, }} + options={{ nestedValuesAsYaml: true }} /> ); }; diff --git a/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx b/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx index d69ab15f1a..3e4ff56a35 100644 --- a/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx @@ -40,20 +40,20 @@ describe('StatefulSetDrawer', () => { expect(getByText('StatefulSet')).toBeInTheDocument(); expect(getByText('YAML')).toBeInTheDocument(); expect( - getByText(textContentMatcher('Type: RollingUpdate')), + getByText(textContentMatcher('type: RollingUpdate')), ).toBeInTheDocument(); - expect(getByText('Rolling Update:')).toBeInTheDocument(); - expect(getByText(textContentMatcher('Max Surge: 25%'))).toBeInTheDocument(); + expect(getByText('rollingUpdate:')).toBeInTheDocument(); + expect(getByText(textContentMatcher('maxSurge: 25%'))).toBeInTheDocument(); expect( - getByText(textContentMatcher('Max Unavailable: 25%')), + getByText(textContentMatcher('maxUnavailable: 25%')), ).toBeInTheDocument(); expect(getByText('Pod Management Policy')).toBeInTheDocument(); expect(getByText('Parallel')).toBeInTheDocument(); expect(getByText('Service Name')).toBeInTheDocument(); expect(getByText('Selector')).toBeInTheDocument(); - expect(getByText('Match Labels:')).toBeInTheDocument(); + expect(getByText('matchLabels:')).toBeInTheDocument(); expect( - getByText(textContentMatcher('App: dice-roller')), + getByText(textContentMatcher('app: dice-roller')), ).toBeInTheDocument(); expect(getByText('Revision History Limit')).toBeInTheDocument(); expect(getByText('10')).toBeInTheDocument(); diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index 2de5a17f25..91b97cbfc5 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -34,7 +34,10 @@ import fixture2 from '../src/__fixtures__/2-deployments.json'; import fixture3 from '../src/__fixtures__/1-cronjobs.json'; import fixture4 from '../src/__fixtures__/2-cronjobs.json'; import fixture5 from '../src/__fixtures__/1-rollouts.json'; +import fixture6 from '../src/__fixtures__/3-ingresses.json'; +import fixture7 from '../src/__fixtures__/2-statefulsets.json'; import { TestApiProvider } from '@backstage/test-utils'; +import { StructuredMetadataTable } from '@backstage/core-components'; const mockEntity: Entity = { apiVersion: 'backstage.io/v1alpha1', @@ -134,6 +137,12 @@ class MockKubernetesClient implements KubernetesApi { } } +const metadata = { + testA: 'stuff', + testB: { testC: 'stuff' }, + testD: [{ testE: 'stuff' }], +}; + createDevApp() .addPage({ path: '/fixture-1', @@ -200,5 +209,35 @@ createDevApp() ), }) + .addPage({ + path: '/fixture-6', + title: 'Fixture 6', + element: ( + + + + + + ), + }) + .addPage({ + path: '/fixture-7', + title: 'Fixture 7', + element: ( + + + + + + + ), + }) .registerPlugin(kubernetesPlugin) .render(); diff --git a/plugins/kubernetes/src/__fixtures__/3-ingresses.json b/plugins/kubernetes/src/__fixtures__/3-ingresses.json new file mode 100644 index 0000000000..94359c8239 --- /dev/null +++ b/plugins/kubernetes/src/__fixtures__/3-ingresses.json @@ -0,0 +1,372 @@ +{ + "ingresses": [ + { + "metadata": { + "name": "app-service-endpoint.dice-roller.backstage.io", + "namespace": "dice-roller", + "uid": "00000000-0000-0000-0000-000000000000", + "resourceVersion": "4749260703", + "generation": 1, + "creationTimestamp": "2023-08-16T14:52:16Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "ingress-class": "istio-internal" + }, + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "spec", + "metacontroller.k8s.io/last-applied-configuration": "spec", + "nginx.ingress.kubernetes.io/auth-signin": "https://backstage.io/authenticate", + "nginx.ingress.kubernetes.io/configuration-snippet": "config", + "nginx.ingress.kubernetes.io/service-upstream": "true", + "nginx.ingress.kubernetes.io/upstream-vhost": "app-service-endpoint.dice-roller.backstage.services" + }, + "ownerReferences": [ + { + "apiVersion": "networking.backstage.com/v1beta1", + "kind": "MeshIntegration", + "name": "app-service-endpoint", + "uid": "00000000-0000-0000-0000-000000000000", + "controller": true, + "blockOwnerDeletion": true + } + ], + "managedFields": [ + { + "manager": "nginx-ingress-controller", + "operation": "Update", + "apiVersion": "networking.k8s.io/v1", + "time": "2023-08-16T14:52:56Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:loadBalancer": { + "f:ingress": {} + } + } + }, + "subresource": "status" + }, + { + "manager": "kubectl-client-side-apply", + "operation": "Update", + "apiVersion": "networking.k8s.io/v1", + "time": "2023-12-15T16:33:17Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {}, + "f:nginx.ingress.kubernetes.io/auth-signin": {}, + "f:nginx.ingress.kubernetes.io/configuration-snippet": {}, + "f:nginx.ingress.kubernetes.io/service-upstream": {}, + "f:nginx.ingress.kubernetes.io/upstream-vhost": {} + }, + "f:labels": { + ".": {}, + "f:backstage.io/kubernetes-id": {}, + "f:ingress-class": {} + } + }, + "f:spec": { + "f:ingressClassName": {}, + "f:rules": {}, + "f:tls": {} + } + } + }, + { + "manager": "kubectl-label", + "operation": "Update", + "apiVersion": "networking.k8s.io/v1", + "time": "2024-02-21T15:00:44Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:controller-uid": {} + } + } + } + }, + { + "manager": "metacontroller", + "operation": "Update", + "apiVersion": "networking.k8s.io/v1", + "time": "2024-02-21T15:01:01Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:metacontroller.k8s.io/last-applied-configuration": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"00000000-0000-0000-0000-000000000000\"}": {} + } + } + } + } + ] + }, + "spec": { + "ingressClassName": "istio-internal", + "tls": { + "hosts": ["app-service-endpoint.dice-roller.backstage.io"], + "secretName": "dice-roller-wildcard-certificate" + }, + "rules": [ + { + "host": "app-service-endpoint.dice-roller.backstage.io", + "http": { + "paths": [ + { + "pathType": "ImplementationSpecific", + "backend": { + "service": { + "name": "app-service-endpoint", + "port": { + "number": 8080 + } + } + } + }, + { + "pathType": "Second item in the list", + "backend": { + "service": { + "name": "app-service-endpoint", + "port": { + "number": 8080 + } + } + } + } + ] + } + } + ] + }, + "status": { + "loadBalancer": { + "ingress": [ + { + "ip": "1.2.3.4" + } + ] + } + } + } + ], + "deployments": [ + { + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "2", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"replicas\":10,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n" + }, + "creationTimestamp": "2020-09-23T12:00:55.000Z", + "generation": 3, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "managedFields": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:progressDeadlineSeconds": {}, + "f:replicas": {}, + "f:revisionHistoryLimit": {}, + "f:selector": { + "f:matchLabels": { + ".": {}, + "f:app": {} + } + }, + "f:strategy": { + "f:rollingUpdate": { + ".": {}, + "f:maxSurge": {}, + "f:maxUnavailable": {} + }, + "f:type": {} + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + } + } + }, + "manager": "kubectl", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:deployment.kubernetes.io/revision": {} + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:conditions": { + ".": {}, + "k:{\"type\":\"Available\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Progressing\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {}, + "f:updatedReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "593230", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/dice-roller", + "uid": "7551e949-42d1-4061-83c5-9da107186e47" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 10, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "dice-roller" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller" + } + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 10, + "conditions": [ + { + "lastTransitionTime": "2020-09-23T12:00:55.000Z", + "lastUpdateTime": "2020-09-24T11:39:28.000Z", + "message": "ReplicaSet \"dice-roller-6c8646bfd\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "lastUpdateTime": "2020-09-25T09:58:55.000Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + } + ], + "observedGeneration": 3, + "readyReplicas": 10, + "replicas": 10, + "updatedReplicas": 10 + } + } + ] +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index e368bfb07d..2239583cb7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -991,6 +991,27 @@ describe('NunjucksWorkflowRunner', () => { ); expect(fakeActionHandler).not.toHaveBeenCalled(); }); + + it('should validate each parameter renders to a valid value', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + each: '${{parameters.data}}', + action: 'jest-validated-action', + input: { foo: '${{each.value}}' }, + }, + ], + output: {}, + parameters: {}, + }); + await expect(runner.execute(task)).rejects.toThrow( + 'Invalid value on action jest-validated-action.each parameter, "${{parameters.data}}" cannot be resolved to a value', + ); + expect(fakeActionHandler).not.toHaveBeenCalled(); + }); }); describe('secrets', () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index fbe87780c7..63d4172659 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -310,13 +310,21 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { return; } } + + const resolvedEach = + step.each && this.render(step.each, context, renderTemplate); + + if (step.each && !resolvedEach) { + throw new InputError( + `Invalid value on action ${action.id}.each parameter, "${step.each}" cannot be resolved to a value`, + ); + } + const iterations = ( - step.each - ? Object.entries(this.render(step.each, context, renderTemplate)).map( - ([key, value]) => ({ - each: { key, value }, - }), - ) + resolvedEach + ? Object.entries(resolvedEach).map(([key, value]) => ({ + each: { key, value }, + })) : [{}] ).map(i => ({ ...i, diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index ffcc377995..97047d00c9 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -20,7 +20,6 @@ import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import Paper from '@material-ui/core/Paper'; import { makeStyles } from '@material-ui/core/styles'; -import { ResizableBox } from 'react-resizable'; import { ScaffolderTaskOutput, scaffolderApiRef, @@ -119,6 +118,7 @@ export const OngoingTask = (props: { useEffect(() => { if (taskStream.completed && !taskStream.error) { + setLogVisibleState(true); setButtonBarVisibleState(false); } }, [taskStream.error, taskStream.completed]); @@ -292,13 +292,11 @@ export const OngoingTask = (props: { ) : null} {logsVisible ? ( - - - - - - - + + + + + ) : null} diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index e0e1c1761e..a9ac353753 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -170,6 +170,7 @@ const techDocsEntityContent = EntityContentBlueprint.makeWithOverrides({ { defaultPath: 'docs', defaultTitle: 'TechDocs', + routeRef: convertLegacyRouteRef(rootCatalogDocsRouteRef), loader: () => import('./Router').then(({ EmbeddedDocsRouter }) => compatWrapper( diff --git a/yarn.lock b/yarn.lock index cdbc53f9e8..158724d133 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4291,6 +4291,7 @@ __metadata: d3-zoom: ^3.0.0 dagre: ^0.8.5 history: ^5.0.0 + js-yaml: ^4.1.0 linkify-react: 4.1.3 linkifyjs: 4.1.3 lodash: ^4.17.21 @@ -19314,13 +19315,13 @@ __metadata: linkType: hard "@types/dockerode@npm:^3.3.0, @types/dockerode@npm:^3.3.29": - version: 3.3.33 - resolution: "@types/dockerode@npm:3.3.33" + version: 3.3.34 + resolution: "@types/dockerode@npm:3.3.34" dependencies: "@types/docker-modem": "*" "@types/node": "*" "@types/ssh2": "*" - checksum: 4e3ed41f3b89a213c9ba923862b653f54ee7232b2c95553410d0c3846396efb592d0291fa2f50d4b47d42a2ae0b49f422e72efcdd845d0ebb60da2e8990e9293 + checksum: 0cb35276fc76ee8ae9d6db7054198e23370f753894d06aa56efbf232032009c6f63a3bfce9d71e6272f3240040d9da2d2a56671d24f34e598ff2000df946ecbd languageName: node linkType: hard @@ -20735,11 +20736,11 @@ __metadata: linkType: hard "@types/ws@npm:*, @types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10, @types/ws@npm:^8.5.3, @types/ws@npm:^8.5.4": - version: 8.5.13 - resolution: "@types/ws@npm:8.5.13" + version: 8.5.14 + resolution: "@types/ws@npm:8.5.14" dependencies: "@types/node": "*" - checksum: f17023ce7b89c6124249c90211803a4aaa02886e12bc2d0d2cd47fa665eeb058db4d871ce4397d8e423f6beea97dd56835dd3fdbb921030fe4d887601e37d609 + checksum: b63d25146a0d2ebb9cb35e4b68c5a01d81b8f4657d62b0a0d9470df6a357798349a44064b2f84b8f98a553ba84d9a5ee302d3053feef02c7771010c55d290ca6 languageName: node linkType: hard @@ -24495,7 +24496,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.4.1, chalk@npm:~5.4.1": +"chalk@npm:^5.4.1": version: 5.4.1 resolution: "chalk@npm:5.4.1" checksum: 0c656f30b782fed4d99198825c0860158901f449a6b12b818b0aabad27ec970389e7e8767d0e00762175b23620c812e70c4fd92c0210e55fc2d993638b74e86e @@ -25167,10 +25168,10 @@ __metadata: languageName: node linkType: hard -"commander@npm:*, commander@npm:^12.0.0, commander@npm:^12.1.0, commander@npm:~12.1.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 68e9818b00fc1ed9cdab9eb16905551c2b768a317ae69a5e3c43924c2b20ac9bb65b27e1cab36aeda7b6496376d4da908996ba2c0b5d79463e0fb1e77935d514 +"commander@npm:*, commander@npm:^13.1.0": + version: 13.1.0 + resolution: "commander@npm:13.1.0" + checksum: 8ca2fcb33caf2aa06fba3722d7a9440921331d54019dabf906f3603313e7bf334b009b862257b44083ff65d5a3ab19e83ad73af282bd5319f01dc228bdf87ef0 languageName: node linkType: hard @@ -25209,6 +25210,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.0.0, commander@npm:^12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 68e9818b00fc1ed9cdab9eb16905551c2b768a317ae69a5e3c43924c2b20ac9bb65b27e1cab36aeda7b6496376d4da908996ba2c0b5d79463e0fb1e77935d514 + languageName: node + linkType: hard + "commander@npm:^2.19.0, commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -26459,7 +26467,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:~4.4.0": +"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -28836,7 +28844,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:~8.0.1": +"execa@npm:^8.0.1": version: 8.0.1 resolution: "execa@npm:8.0.1" dependencies: @@ -34713,7 +34721,7 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:~3.1.3": +"lilconfig@npm:^3.1.3": version: 3.1.3 resolution: "lilconfig@npm:3.1.3" checksum: 644eb10830350f9cdc88610f71a921f510574ed02424b57b0b3abb66ea725d7a082559552524a842f4e0272c196b88dfe1ff7d35ffcc6f45736777185cd67c9a @@ -34763,22 +34771,22 @@ __metadata: linkType: hard "lint-staged@npm:^15.0.0": - version: 15.4.1 - resolution: "lint-staged@npm:15.4.1" + version: 15.4.2 + resolution: "lint-staged@npm:15.4.2" dependencies: - chalk: ~5.4.1 - commander: ~12.1.0 - debug: ~4.4.0 - execa: ~8.0.1 - lilconfig: ~3.1.3 - listr2: ~8.2.5 - micromatch: ~4.0.8 - pidtree: ~0.6.0 - string-argv: ~0.3.2 - yaml: ~2.6.1 + chalk: ^5.4.1 + commander: ^13.1.0 + debug: ^4.4.0 + execa: ^8.0.1 + lilconfig: ^3.1.3 + listr2: ^8.2.5 + micromatch: ^4.0.8 + pidtree: ^0.6.0 + string-argv: ^0.3.2 + yaml: ^2.7.0 bin: lint-staged: bin/lint-staged.js - checksum: 6c17747e9379a08ad3d15aab869a362c750d357182c76ca88121f01143910a2a2d5b1673536f3ba9db2331bf2102ffee115532736dd9c3fa2696344732a47d3d + checksum: bdb031d64f388aea166b3beaaff8a0a1503e50433959805985b43fcd788c9bfaf1e3a6c0852d830a6c75345b9c0809c5e6dab80d48460e3d6317b7ca0f842df8 languageName: node linkType: hard @@ -34789,7 +34797,7 @@ __metadata: languageName: node linkType: hard -"listr2@npm:~8.2.5": +"listr2@npm:^8.2.5": version: 8.2.5 resolution: "listr2@npm:8.2.5" dependencies: @@ -36266,7 +36274,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:^4.0.7, micromatch@npm:^4.0.8, micromatch@npm:~4.0.8": +"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:^4.0.7, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -39212,7 +39220,7 @@ __metadata: languageName: node linkType: hard -"pidtree@npm:~0.6.0": +"pidtree@npm:^0.6.0": version: 0.6.0 resolution: "pidtree@npm:0.6.0" bin: @@ -43959,7 +43967,7 @@ __metadata: languageName: node linkType: hard -"string-argv@npm:~0.3.1, string-argv@npm:~0.3.2": +"string-argv@npm:^0.3.2, string-argv@npm:~0.3.1": version: 0.3.2 resolution: "string-argv@npm:0.3.2" checksum: 8703ad3f3db0b2641ed2adbb15cf24d3945070d9a751f9e74a924966db9f325ac755169007233e8985a39a6a292f14d4fee20482989b89b96e473c4221508a0f @@ -47657,12 +47665,12 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3, yaml@npm:^2.3.4, yaml@npm:~2.6.1": - version: 2.6.1 - resolution: "yaml@npm:2.6.1" +"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3, yaml@npm:^2.3.4, yaml@npm:^2.7.0": + version: 2.7.0 + resolution: "yaml@npm:2.7.0" bin: yaml: bin.mjs - checksum: 5cf2627f121dcf04ccdebce8e6cbac7c9983d465c4eab314f6fbdc13cda8a07f4e8f9c2252a382b30bcabe05ee3c683647293afd52eb37cbcefbdc7b6ebde9ee + checksum: 6e8b2f9b9d1b18b10274d58eb3a47ec223d9a93245a890dcb34d62865f7e744747190a9b9177d5f0ef4ea2e44ad2c0214993deb42e0800766203ac46f00a12dd languageName: node linkType: hard