feat(ui): add Status component - #1849
Conversation
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
add `isDataGrid` var so elements can detect whether they are in a DataGrid or not Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
🦋 Changeset detectedLatest commit: bcf227f The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
There was a problem hiding this comment.
Pull request overview
Adds a new reusable Status component to ui-components for displaying non-data states (progress, error, empty, no-matches), including DataGrid-aware rendering, with accompanying Storybook stories, tests, and theme tokens to support its visuals.
Changes:
- Introduces
Statuscomponent with defaults, optional spinner/details/action slots, and automatic compact styling when rendered inside aDataGrid. - Adds Storybook stories and a new test suite covering roles, defaults, spinner behavior, details, action slot, and DataGrid behavior.
- Extends theme/global CSS tokens for
Statusdetails/code styling, and updatesDataGridcontext to expose anisDataGridsignal.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ui-components/src/theme.css | Adds theme tokens for Status details background/text/border and code color (light/dark). |
| packages/ui-components/src/global.css | Mirrors Status-related tokens for global theming. |
| packages/ui-components/src/components/Status/Status.component.tsx | Implements the Status component (defaults, HTTP code copy, DataGrid context behavior, styling). |
| packages/ui-components/src/components/Status/Status.test.tsx | Adds unit tests for Status rendering behavior and DataGrid context behavior. |
| packages/ui-components/src/components/Status/Status.stories.tsx | Adds Storybook docs + examples for page and DataGrid usage. |
| packages/ui-components/src/components/Status/index.ts | Exports Status and StatusProps. |
| packages/ui-components/src/components/DataGrid/DataGrid.component.tsx | Adds isDataGrid to context so consumer components can detect DataGrid presence. |
Comments suppressed due to low confidence (2)
packages/ui-components/src/components/Status/Status.component.tsx:118
- When
codeis provided but isn’t a known numeric HTTP code (e.g. the docs’XXXunknown case, or any unmapped code), the component currently ignores the HTTP reference defaults entirely and falls back to the generic status defaults. Consider applying an explicit “Unknown Error” fallback whenevercodeis set but not matched.
const numericCode = typeof code === "string" ? parseInt(code, 10) : code
const httpDefaults = numericCode ? HTTP_ERRORS[numericCode] : undefined
packages/ui-components/src/components/Status/Status.component.tsx:13
- The default copy for HTTP 404 doesn’t match the UX doc reference (it omits the guidance to check the URL / return home), so
code={404}will render different text than specified.
404: { title: "Page Not Found", body: "The requested URL does not exist or may have moved." },
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
packages/ui-components/src/components/Status/Status.component.tsx:13
- The HTTP error code reference in
docs/ux/error-handling-loading-empty-states.mdincludes copy for 400 (Bad Request), butHTTP_ERRORSis missing a 400 entry, socode={400}won’t resolve documented defaults.
const HTTP_ERRORS: Record<number, { title: string; body: string }> = {
400: { title: "Bad Request", body: "The request could not be processed due to invalid syntax. Try again." },
401: { title: "Authentication Required", body: "Authentication failed. Verify your credentials and try again." },
403: { title: "Access Denied", body: "You do not have the required permissions to access this resource." },
packages/ui-components/src/components/Status/Status.component.tsx:35
Statusforwards...propsto the root<div>(and tests passdata-testid), butStatusPropsdoes not extendHTMLAttributes<HTMLDivElement>. This makes common div props a TypeScript error for consumers.
export interface StatusProps extends React.HTMLAttributes<HTMLDivElement> {
/** The status to display. Determines the default copy. Defaults to `"error"`. */
packages/ui-components/src/components/Status/Status.component.tsx:102
detailsis documented to “scroll vertically if content exceeds the maximum height”, but the basedetailsStylesonly sets horizontal scrolling. Outside a DataGrid, longdetailswill expand the page instead of y-scrolling because there’s no max-height +overflow-y-auto.
const detailsStyles = `
jn:text-left
jn:text-xs
jn:bg-theme-status-details
jn:text-theme-status-details
jn:border
jn:border-theme-status-details
jn:py-0.5
jn:px-1
jn:mt-4
jn:w-full
jn:max-w-[50rem]
jn:overflow-x-auto
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
packages/ui-components/src/components/Status/Status.component.tsx:143
detailscan be rendered for any status, but the accessible label is always "Error details". This is misleading for non-error states (and for generic diagnostic output). Consider a status-aware label or a neutral label.
<pre
aria-label="Error details"
className={`juno-status-details ${detailsStyles} ${isDataGrid ? detailsDataGridStyles : ""}`}
packages/ui-components/src/components/Status/Status.component.tsx:137
Spinnergets an emptyaria-labelwhen the resolved title is an empty string (e.g.status="no-matches"has a default title of "" andspinner={true}is allowed). An empty accessible name is invalid and makes the progress indicator harder to interpret for assistive tech. Use a falsy check instead of nullish coalescing so the fallback is used for "" as well.
This issue also appears on line 141 of the same file.
{resolvedSpinner && <Spinner variant="primary" aria-label={resolvedTitle ?? "Loading"} />}
packages/ui-components/src/components/Status/Status.component.tsx:14
- The default copy for HTTP 404 doesn’t match the UX docs reference (docs/ux/error-handling-loading-empty-states.md:148 includes the additional guidance "Check the URL or return to the home page."). Since the issue scope says error defaults should follow that reference, aligning the message here will keep
Statusconsistent with the docs.
404: { title: "Page Not Found", body: "The requested URL does not exist or may have moved." },
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (3)
packages/ui-components/src/components/Status/Status.component.tsx:18
- The 404 default body text here doesn’t match the UX docs’ HTTP error code reference (docs/ux/error-handling-loading-empty-states.md shows an additional “Check the URL or return to the home page.” sentence). This causes
status="error" code={404}to render different copy than documented.
403: { title: "Access Denied", body: "You do not have the required permissions to access this resource." },
404: { title: "Page Not Found", body: "The requested URL does not exist or may have moved." },
408: {
title: "Request Timeout",
packages/ui-components/src/components/Status/Status.component.tsx:139
aria-label={resolvedTitle ?? "Loading"}can produce an empty accessible name whenresolvedTitleis an empty string (e.g. the defaultno-matchestitle is ""). This leaves the Spinner/progressbar unlabeled for assistive tech whenspinneris enabled.
{code && !isDataGrid && <div className={`juno-status-code ${codeStyles}`}>{code}</div>}
{resolvedSpinner && <Spinner variant="primary" aria-label={resolvedTitle ?? "Loading"} />}
{resolvedTitle && <strong className={`juno-status-title ${titleStyles}`}>{resolvedTitle}</strong>}
packages/ui-components/src/components/Status/index.ts:6
Statusis added with its own barrel export, but it isn’t exported from the package entrypoint (packages/ui-components/src/index.ts). As a result, consumers importing from@cloudoperators/juno-ui-componentswon’t be able to access the new component via the standard public API surface.
export { Status, type StatusProps } from "./Status.component"
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (4)
packages/ui-components/src/components/Status/Status.component.tsx:40
StatusPropsextendsReact.HTMLAttributes<HTMLDivElement>, which already includes the standard HTMLtitleattribute (tooltip). Reusingtitlefor the component’s heading both overloads the meaning and prevents passing a realtitleattribute through to the root element (since it’s destructured out). Consider renaming the component prop (e.g.,heading) and/or changing the props type toOmit<React.HTMLAttributes<HTMLDivElement>, "title">to avoid the collision.
export interface StatusProps extends React.HTMLAttributes<HTMLDivElement> {
/** The status to display. Determines the default copy. Defaults to `"error"`. */
status?: "progress" | "error" | "empty" | "no-matches"
/** Optional title. Overrides the per-status default title when set. */
title?: string
/** Optional body text. Overrides the per-status default body text when set. */
body?: string
packages/ui-components/src/components/Status/Status.component.tsx:134
- Because
{...props}is spread afterrole={role}, a consumer-providedrolewill override the component’s computed role (and can unintentionally break the intended accessibility semantics and test expectations). To make the role deterministic, either spreadpropsearlier (e.g., first) and then setrole, or explicitly omitrolefrom...props.
return (
<div
role={role}
className={`juno-status ${isDataGrid ? `juno-status-datagrid ${statusDataGridStyles}` : ""} ${statusStyles} ${className}`}
{...props}
>
packages/ui-components/src/components/Status/Status.component.tsx:119
parseIntwill accept partial numeric prefixes (e.g.,"404 Not Found"becomes404), which can unintentionally trigger HTTP defaults even whencodeis not a pure numeric code string. Consider only parsing when the string is strictly digits (or usingNumber()+Number.isFinite) so defaults are applied only for valid numeric HTTP codes.
const numericCode = typeof code === "string" ? parseInt(code, 10) : code
const httpDefaults = numericCode ? HTTP_ERRORS[numericCode] : undefined
packages/ui-components/src/components/Status/Status.component.tsx:32
STATUS_DEFAULTSis typed asRecord<string, ...>, which removes compile-time guarantees that all supportedstatusvalues are present (and only those). Tighten the type to thestatusunion (e.g.,Record<NonNullable<StatusProps["status"]>, ...>orsatisfies Record<...>) to prevent drift if statuses are added/renamed later.
const STATUS_DEFAULTS: Record<string, { title: string; body: string }> = {
progress: { title: "Loading…", body: "" },
error: { title: "Something went wrong", body: "An error occurred. Try again." },
empty: { title: "No items", body: "There are no items to display." },
"no-matches": { title: "", body: "No items match the current filters. Adjust or clear filters." },
}
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
Signed-off-by: Franz Heidl <franz.heidl@sap.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (3)
packages/ui-components/src/components/Status/Status.component.tsx:144
detailscan be rendered for any status, but the<pre>is always labeled "Error details". This is inaccurate for non-error statuses and can confuse assistive tech users. Consider using a neutral label or deriving it fromstatus.
<pre
aria-label="Error details"
className={`juno-status-details ${detailsStyles} ${isDataGrid ? detailsDataGridStyles : ""}`}
>
docs/ux/datagrid.md:109
- Typo in image alt text: "compponent" → "component".

packages/ui-components/src/components/Status/index.ts:6
Statusis added undercomponents/Status, but it isn’t exported from the package entrypoint (packages/ui-components/src/index.ts). As a result, consumers can’t import it from@cloudoperators/juno-ui-componentsthe same way they do for other components.
export { Status, type StatusProps } from "./Status.component"
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (7)
packages/ui-components/src/components/Status/Status.component.tsx:32
STATUS_DEFAULTS["no-matches"]sets an empty string title, which results in no title being rendered (because the render guard isresolvedTitle && ...). The issue/spec calls for sensible default title/body per status, sono-matchesshould also provide a non-empty default title.
const STATUS_DEFAULTS: Record<string, { title: string; body: string }> = {
progress: { title: "Loading…", body: "" },
error: { title: "Something went wrong", body: "An error occurred. Try again." },
empty: { title: "No items", body: "There are no items to display." },
"no-matches": { title: "", body: "No items match the current filters. Adjust or clear filters." },
}
packages/ui-components/src/components/Status/Status.test.tsx:75
- This test currently asserts the 404 body text that’s implemented in
HTTP_ERRORS, but that copy diverges from the UX docs’ HTTP error code reference. If the component is updated to match the documented 404 message, this expectation should be updated accordingly.
it("renders title and body from HTTP error code", () => {
render(<Status status="error" code={404} />)
expect(screen.getByText("Page Not Found")).toBeInTheDocument()
expect(screen.getByText("The requested URL does not exist or may have moved.")).toBeInTheDocument()
})
packages/ui-components/src/components/Status/Status.component.tsx:144
detailscan be provided regardless ofstatus, but the accessible label is hard-coded to “Error details”. This can be misleading for non-error use cases (and makes it harder to reuse the component). Use a neutral label like “Details”.
<pre
aria-label="Error details"
className={`juno-status-details ${detailsStyles} ${isDataGrid ? detailsDataGridStyles : ""}`}
>
packages/ui-components/src/components/Status/Status.test.tsx:96
- If the
Statuscomponent’s details block label is made neutral (e.g. “Details”), the test should assert the updated label instead of “Error details”.
it("renders details in a pre element", () => {
render(<Status details="Error: something failed" />)
expect(screen.getByLabelText("Error details")).toBeInTheDocument()
expect(screen.getByText("Error: something failed")).toBeInTheDocument()
})
it("does not render details when not passed", () => {
render(<Status />)
expect(screen.queryByLabelText("Error details")).not.toBeInTheDocument()
})
packages/ui-components/src/components/Status/Status.component.tsx:15
- The default copy for HTTP 404 does not match the UX docs’ HTTP error code reference (
docs/ux/error-handling-loading-empty-states.mdincludes the additional guidance “Check the URL or return to the home page.”). Since the issue/PR states defaults should follow that reference, the 404 body should be aligned (or the docs updated).
This issue also appears in the following locations of the same file:
- line 27
- line 141
403: { title: "Access Denied", body: "You do not have the required permissions to access this resource." },
404: { title: "Page Not Found", body: "The requested URL does not exist or may have moved." },
408: {
packages/ui-components/src/components/Status/Status.test.tsx:69
- There’s no test covering the
no-matchesstatus defaults. SinceStatushas status-specific default copy, adding ano-matchesassertion helps prevent regressions (especially after changing defaults).
This issue also appears in the following locations of the same file:
- line 71
- line 87
it("renders the default title for the empty status", () => {
render(<Status status="empty" />)
expect(screen.getByRole("status")).toHaveTextContent("No items")
})
docs/ux/datagrid.md:109
- Typo in image alt text: “compponent” → “component”.

to be refined Signed-off-by: Franz Heidl <franz.heidl@sap.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (5)
packages/ui-components/src/components/Status/Status.component.tsx:137
Spinner'saria-labelusesresolvedTitle ?? "Loading". Forstatus="no-matches", the default title is an empty string, so if a caller enablesspinner, the label becomes an empty string (because??does not treat "" as missing), which is not accessible. Use a fallback that treats empty strings as missing.
{resolvedSpinner && <Spinner variant="primary" aria-label={resolvedTitle ?? "Loading"} />}
packages/ui-components/src/components/Status/Status.component.tsx:106
Statusis implemented and has a localcomponents/Status/index.ts, but it is not exported from the package entrypoint (packages/ui-components/src/index.ts). As a result, consumers importing from@cloudoperators/juno-ui-componentswon’t be able to use it without a deep import.
export const Status = ({
status = "error",
packages/ui-components/src/components/Status/Status.test.tsx:70
- There is no test covering the
no-matchesdefault copy/structure (notably the intentional empty default title). Adding a test will prevent regressions where a blank title accidentally renders, or the default body copy changes unexpectedly.
it("renders the default title for the empty status", () => {
render(<Status status="empty" />)
expect(screen.getByRole("status")).toHaveTextContent("No items")
})
docs/ux/datagrid.md:109
- Typo in the image alt text: "compponent" → "component".

packages/ui-components/src/components/Status/Status.component.tsx:103
detailsis documented to “scroll vertically if content exceeds the maximum height”, but outside aDataGridthe<pre>has nomax-heightand nooverflow-y-auto, so long stack traces will expand the page instead of scrolling. Consider adding a max-height + vertical scrolling to the basedetailsStyles(and keep the DataGrid-specificmin-h-0if needed for flexbox scrolling).
const detailsStyles = `
jn:text-left
jn:text-xs
jn:bg-theme-status-details
jn:text-theme-status-details
jn:border
jn:border-theme-status-details
jn:py-0.5
jn:px-1
jn:mt-4
jn:w-full
jn:max-w-[50rem]
jn:overflow-x-auto
`
Summary
This PR implements a generally usable
Statuscomponent to render error, loading, no items, no matches etc.Changes Made
Statuscomponent, stories, testsRelated Issues
Closes #1828
Testing Instructions
pnpm ipnpm run test StatusChecklist
PR Manifesto
Review the PR Manifesto for best practises.