From ba9e64681c5ca1b71e7552fd7b8ec32d8325bf27 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 14:40:58 +0200 Subject: [PATCH 01/20] feat(ui): add Status component stub, stories, tests Signed-off-by: Franz Heidl --- .../components/Status/Status.component.tsx | 81 +++++++++++++ .../src/components/Status/Status.stories.tsx | 106 ++++++++++++++++++ .../src/components/Status/Status.test.tsx | 15 +++ .../src/components/Status/index.ts | 6 + 4 files changed, 208 insertions(+) create mode 100644 packages/ui-components/src/components/Status/Status.component.tsx create mode 100644 packages/ui-components/src/components/Status/Status.stories.tsx create mode 100644 packages/ui-components/src/components/Status/Status.test.tsx create mode 100644 packages/ui-components/src/components/Status/index.ts diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx new file mode 100644 index 0000000000..bb36a3063d --- /dev/null +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactNode } from "react" +import { Spinner } from "../Spinner" + +const HTTP_ERRORS: Record = { + 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." }, + 404: { title: "Page Not Found", body: "The requested URL does not exist or may have moved." }, + 408: { + title: "Request Timeout", + body: "The request did not return a complete result in time. Check your connection and try again.", + }, + 409: { title: "Conflict", body: "" }, + 429: { title: "Too Many Requests", body: "" }, + 500: { title: "Internal Server Error", body: "An internal error occurred. Try again." }, + 502: { title: "Bad Gateway", body: "The server returned an invalid response. Try again." }, + 503: { title: "Service Unavailable", body: "The service is temporarily unavailable. Try again later." }, + 504: { title: "Gateway Timeout", body: "A server did not respond in time. Check your connection and try again." }, +} + +const STATUS_DEFAULTS: Record = { + progress: { title: "Loading…", body: "" }, + error: { title: "Something went wrong", body: "An error occurred. Try again." }, + empty: { title: "No items", body: "" }, + "no-matches": { title: "No results", body: "No items match your current filters." }, +} + +export interface StatusProps { + /** The status to display. Determines the default icon, color, and copy. */ + 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 + /** Renders a `Spinner`. Defaults to `true` when `status="progress"`, `false` otherwise. */ + spinner?: boolean + /** Displayed large and prominently above the title. Intended for HTTP error codes such as 404 or 500. */ + code?: number | string + /** Rendered in a `
` block using monospaced font. Intended for stack traces and server responses. Scrolls vertically if content exceeds the maximum height. */
+  details?: string
+  /** Optional action area rendered below the content. Typically a `Button` or a button-styled anchor element. */
+  action?: ReactNode
+  /** Add custom CSS classes to the root element. */
+  className?: string
+}
+
+export const Status = ({
+  status,
+  title,
+  body,
+  spinner,
+  code,
+  details,
+  action,
+  className = "",
+  ...props
+}: StatusProps) => {
+  const numericCode = typeof code === "string" ? parseInt(code, 10) : code
+  const httpDefaults = numericCode ? HTTP_ERRORS[numericCode] : undefined
+  const statusDefaults = status ? STATUS_DEFAULTS[status] : undefined
+
+  const resolvedTitle = title ?? httpDefaults?.title ?? statusDefaults?.title
+  const resolvedBody = body ?? httpDefaults?.body ?? statusDefaults?.body
+
+  const resolvedSpinner = spinner ?? status === "progress"
+
+  return (
+    
+ {code &&
{code}
} + {resolvedSpinner && } + {resolvedTitle &&
{resolvedTitle}
} + {resolvedBody &&
{resolvedBody}
} + {details &&
{details}
} + {action &&
{action}
} +
+ ) +} diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx new file mode 100644 index 0000000000..0d38caa8a7 --- /dev/null +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import type { Meta, StoryObj } from "@storybook/react-vite" +import { Status } from "./index" +import { DataGrid } from "../DataGrid/DataGrid.component" +import { DataGridRow } from "../DataGridRow/DataGridRow.component" +import { DataGridCell } from "../DataGridCell/DataGridCell.component" +import { DataGridHeadCell } from "../DataGridHeadCell/DataGridHeadCell.component" + +const meta: Meta = { + title: "Components/Status", + component: Status, + argTypes: { + status: { + control: "select", + options: ["progress", "error", "empty", "no-matches"], + }, + action: { control: false }, + }, + parameters: { + docs: { + description: { + component: + "`Status` is a general-purpose component for communicating non-data states: in progress, error, empty, and no matches. Use it as the default drop-in whenever a component, view, or data container has no local or specific way to handle these states — it covers application-, page-, and section-level states as well as error boundary fallbacks.\n\nWhen used inside a `DataGrid`, wrap `Status` in a `DataGridRow` and `DataGridCell` with the appropriate `colSpan` — `Status` renders a `
` only and has no table markup of its own.", + }, + }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} + +const DataGridWrapper = (args: React.ComponentProps) => ( + + + Name + Region + Status + Last Updated + + + + + + + +) + +export const DataGridLoading: Story = { + args: { status: "progress" }, + render: (args) => , + parameters: { + docs: { + description: { + story: 'Use `status="progress"` inside a `DataGridRow` spanning all columns while data is being fetched.', + }, + }, + }, +} + +export const DataGridError: Story = { + args: { status: "error" }, + render: (args) => , + parameters: { + docs: { + description: { + story: + 'Use `status="error"` inside a `DataGridRow` spanning all columns when a data fetch or operation has failed.', + }, + }, + }, +} + +export const DataGridEmpty: Story = { + args: { status: "empty" }, + render: (args) => , + parameters: { + docs: { + description: { + story: + 'Use `status="empty"` inside a `DataGridRow` spanning all columns when the data source contains no items.', + }, + }, + }, +} + +export const DataGridNoMatches: Story = { + args: { status: "no-matches" }, + render: (args) => , + parameters: { + docs: { + description: { + story: + 'Use `status="no-matches"` inside a `DataGridRow` spanning all columns when items exist but none match the currently applied filters.', + }, + }, + }, +} diff --git a/packages/ui-components/src/components/Status/Status.test.tsx b/packages/ui-components/src/components/Status/Status.test.tsx new file mode 100644 index 0000000000..b8cc7b1e61 --- /dev/null +++ b/packages/ui-components/src/components/Status/Status.test.tsx @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { render } from "@testing-library/react" +import { Status } from "./Status.component" + +describe("Status", () => { + it("renders", () => { + render() + expect(document.querySelector(".juno-status")).toBeInTheDocument() + }) +}) diff --git a/packages/ui-components/src/components/Status/index.ts b/packages/ui-components/src/components/Status/index.ts new file mode 100644 index 0000000000..07333cdedc --- /dev/null +++ b/packages/ui-components/src/components/Status/index.ts @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { Status, type StatusProps } from "./Status.component" From 6061e59923b62dc4a31e8781a07f97a185772a77 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 14:56:46 +0200 Subject: [PATCH 02/20] feat(ui): always render blue spinner Signed-off-by: Franz Heidl --- .../ui-components/src/components/Status/Status.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx index bb36a3063d..d9ae1c006d 100644 --- a/packages/ui-components/src/components/Status/Status.component.tsx +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -71,7 +71,7 @@ export const Status = ({ return (
{code &&
{code}
} - {resolvedSpinner && } + {resolvedSpinner && } {resolvedTitle &&
{resolvedTitle}
} {resolvedBody &&
{resolvedBody}
} {details &&
{details}
} From 460434802396a86e6e9a3251d570460133ff0230 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 15:17:33 +0200 Subject: [PATCH 03/20] feat(ui): use generic error as default status Signed-off-by: Franz Heidl --- .../ui-components/src/components/Status/Status.component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx index d9ae1c006d..b21ea5c88a 100644 --- a/packages/ui-components/src/components/Status/Status.component.tsx +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -30,7 +30,7 @@ const STATUS_DEFAULTS: Record = { } export interface StatusProps { - /** The status to display. Determines the default icon, color, and copy. */ + /** 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 @@ -49,7 +49,7 @@ export interface StatusProps { } export const Status = ({ - status, + status = "error", title, body, spinner, From 05097ab8f4e48ca4a44144012a5fed55a097e38e Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 15:20:48 +0200 Subject: [PATCH 04/20] feat(ui): add Datagrid code to stories for example visibility Signed-off-by: Franz Heidl --- .../src/components/Status/Status.stories.tsx | 104 ++++++++++++++---- 1 file changed, 85 insertions(+), 19 deletions(-) diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx index 0d38caa8a7..56263ce6bc 100644 --- a/packages/ui-components/src/components/Status/Status.stories.tsx +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -38,25 +38,49 @@ export const Default: Story = { args: {}, } -const DataGridWrapper = (args: React.ComponentProps) => ( - - - Name - Region - Status - Last Updated - - - - - - - -) +export const HttpError: Story = { + args: { status: "error", code: 404 }, + parameters: { + docs: { + description: { + story: "Page-level 404 error. Title and body are derived from the HTTP error code reference.", + }, + }, + }, +} + +export const WithDetails: Story = { + args: { + status: "error", + details: `Error: Failed to fetch resource\n at fetchData (api.ts:42)\n at async loadServices (services.ts:17)`, + }, + parameters: { + docs: { + description: { + story: + "Pass a stack trace or server response via `details` to render it in a scrollable `
` block below the message. Useful for technical users who need the full error context.",
+      },
+    },
+  },
+}
 
 export const DataGridLoading: Story = {
   args: { status: "progress" },
-  render: (args) => ,
+  render: (args) => (
+    
+      
+        Name
+        Region
+        Status
+        Last Updated
+      
+      
+        
+          
+        
+      
+    
+  ),
   parameters: {
     docs: {
       description: {
@@ -68,7 +92,21 @@ export const DataGridLoading: Story = {
 
 export const DataGridError: Story = {
   args: { status: "error" },
-  render: (args) => ,
+  render: (args) => (
+    
+      
+        Name
+        Region
+        Status
+        Last Updated
+      
+      
+        
+          
+        
+      
+    
+  ),
   parameters: {
     docs: {
       description: {
@@ -81,7 +119,21 @@ export const DataGridError: Story = {
 
 export const DataGridEmpty: Story = {
   args: { status: "empty" },
-  render: (args) => ,
+  render: (args) => (
+    
+      
+        Name
+        Region
+        Status
+        Last Updated
+      
+      
+        
+          
+        
+      
+    
+  ),
   parameters: {
     docs: {
       description: {
@@ -94,7 +146,21 @@ export const DataGridEmpty: Story = {
 
 export const DataGridNoMatches: Story = {
   args: { status: "no-matches" },
-  render: (args) => ,
+  render: (args) => (
+    
+      
+        Name
+        Region
+        Status
+        Last Updated
+      
+      
+        
+          
+        
+      
+    
+  ),
   parameters: {
     docs: {
       description: {

From 15435ab3277403f50eaded4f1a26ce17940b55df Mon Sep 17 00:00:00 2001
From: Franz Heidl 
Date: Thu, 30 Jul 2026 15:21:43 +0200
Subject: [PATCH 05/20] feat(ui): add some tests

Signed-off-by: Franz Heidl 
---
 .../src/components/Status/Status.test.tsx     | 31 +++++++++++++++++--
 1 file changed, 29 insertions(+), 2 deletions(-)

diff --git a/packages/ui-components/src/components/Status/Status.test.tsx b/packages/ui-components/src/components/Status/Status.test.tsx
index b8cc7b1e61..26273e4f98 100644
--- a/packages/ui-components/src/components/Status/Status.test.tsx
+++ b/packages/ui-components/src/components/Status/Status.test.tsx
@@ -4,12 +4,39 @@
  */
 
 import React from "react"
-import { render } from "@testing-library/react"
+import { render, screen } from "@testing-library/react"
 import { Status } from "./Status.component"
 
 describe("Status", () => {
-  it("renders", () => {
+  it("renders a Status", () => {
     render()
     expect(document.querySelector(".juno-status")).toBeInTheDocument()
   })
+
+  it("renders a spinner when status is progress", () => {
+    render()
+    expect(screen.getByRole("progressbar")).toBeInTheDocument()
+    expect(screen.getByRole("progressbar")).toHaveClass("juno-spinner")
+  })
+
+  it("does not render a spinner by default for non-progress statuses", () => {
+    render()
+    expect(screen.queryByRole("progressbar")).not.toBeInTheDocument()
+  })
+
+  it("renders a spinner when spinner prop is true", () => {
+    render()
+    expect(screen.getByRole("progressbar")).toBeInTheDocument()
+    expect(screen.getByRole("progressbar")).toHaveClass("juno-spinner")
+  })
+
+  it("does not render a spinner when spinner prop is false even for progress status", () => {
+    render()
+    expect(screen.queryByRole("progressbar")).not.toBeInTheDocument()
+  })
+
+  it("renders the spinner with the primary variant", () => {
+    render()
+    expect(screen.getByRole("progressbar")).toHaveClass("jn:text-theme-accent")
+  })
 })

From b75244166955330dac3e8978ce21b31479e27b90 Mon Sep 17 00:00:00 2001
From: Franz Heidl 
Date: Thu, 30 Jul 2026 15:42:46 +0200
Subject: [PATCH 06/20] feat(ui): add DataGrid context variable

add `isDataGrid` var so elements can detect whether they are in a DataGrid or not

Signed-off-by: Franz Heidl 
---
 .../src/components/DataGrid/DataGrid.component.tsx           | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/packages/ui-components/src/components/DataGrid/DataGrid.component.tsx b/packages/ui-components/src/components/DataGrid/DataGrid.component.tsx
index 2d294fd619..da5d2db935 100644
--- a/packages/ui-components/src/components/DataGrid/DataGrid.component.tsx
+++ b/packages/ui-components/src/components/DataGrid/DataGrid.component.tsx
@@ -46,6 +46,7 @@ const gridTemplate = (
 
 interface DataGridContextType {
   cellVerticalAlignment?: CellVerticalAlignmentType
+  isDataGrid?: boolean
 }
 
 const DataGridContext = createContext({})
@@ -72,6 +73,10 @@ export const DataGrid = ({
 }: DataGridProps): ReactNode => {
   const dataGridConf = {
     cellVerticalAlignment: cellVerticalAlignment,
+    // allows consumer components like Status to detect the DataGrid context.
+    // Note: the context always exists as {} by default, so checking for context presence alone is not sufficient —
+    // isDataGrid: true is the only reliable signal that a DataGrid provider is actually in the tree.
+    isDataGrid: true,
     // selectable: selectable
   }
   return (

From 3e7968bc4d202adf2c014bfdce862434bfc3a3a3 Mon Sep 17 00:00:00 2001
From: Franz Heidl 
Date: Thu, 30 Jul 2026 15:44:50 +0200
Subject: [PATCH 07/20] =?UTF-8?q?feat(ui):=20add=20styles,=20don=E2=80=99t?=
 =?UTF-8?q?=20render=20code=20in=20DataGrid=20context?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Franz Heidl 
---
 .../components/Status/Status.component.tsx    | 21 +++++++++++++++++--
 1 file changed, 19 insertions(+), 2 deletions(-)

diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx
index b21ea5c88a..5f6951d4ee 100644
--- a/packages/ui-components/src/components/Status/Status.component.tsx
+++ b/packages/ui-components/src/components/Status/Status.component.tsx
@@ -5,6 +5,7 @@
 
 import React, { ReactNode } from "react"
 import { Spinner } from "../Spinner"
+import { useDataGridContext } from "../DataGrid/DataGrid.component"
 
 const HTTP_ERRORS: Record = {
   401: { title: "Authentication Required", body: "Authentication failed. Verify your credentials and try again." },
@@ -48,6 +49,19 @@ export interface StatusProps {
   className?: string
 }
 
+const statusStyles = `
+  jn:flex
+  jn:flex-col
+  jn:items-center
+  jn:text-center
+`
+
+const codeStyles = `
+  jn:text-[13rem]
+  jn:font-bold
+  jn:leading-none
+`
+
 export const Status = ({
   status = "error",
   title,
@@ -59,6 +73,8 @@ export const Status = ({
   className = "",
   ...props
 }: StatusProps) => {
+  const { isDataGrid } = useDataGridContext()
+
   const numericCode = typeof code === "string" ? parseInt(code, 10) : code
   const httpDefaults = numericCode ? HTTP_ERRORS[numericCode] : undefined
   const statusDefaults = status ? STATUS_DEFAULTS[status] : undefined
@@ -69,8 +85,9 @@ export const Status = ({
   const resolvedSpinner = spinner ?? status === "progress"
 
   return (
-    
- {code &&
{code}
} +
+ {/* HTTP error codes are not rendered inside a DataGrid */} + {code && !isDataGrid &&
{code}
} {resolvedSpinner && } {resolvedTitle &&
{resolvedTitle}
} {resolvedBody &&
{resolvedBody}
} From a808c1e924931672daadc4e2b5b6da72472ebb49 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 18:59:26 +0200 Subject: [PATCH 08/20] feat(ui): add more styles and stories Signed-off-by: Franz Heidl --- .../components/Status/Status.component.tsx | 35 +++++++++-- .../src/components/Status/Status.stories.tsx | 62 ++++++++++++++++++- .../src/components/Status/Status.test.tsx | 29 +++++++++ packages/ui-components/src/global.css | 8 +++ packages/ui-components/src/theme.css | 9 ++- 5 files changed, 137 insertions(+), 6 deletions(-) diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx index 5f6951d4ee..c3c7bc6ba4 100644 --- a/packages/ui-components/src/components/Status/Status.component.tsx +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -56,12 +56,32 @@ const statusStyles = ` jn:text-center ` +const statusDataGridStyles = ` + jn:min-h-[12.5rem] + jn:max-h-[18.1875rem] + jn:justify-center +` + +const detailsDataGridStyles = ` + jn:min-h-0 + jn:overflow-y-auto +` + const codeStyles = ` - jn:text-[13rem] + jn:text-[12.5rem] jn:font-bold jn:leading-none ` +const detailsStyles = ` + jn:text-left + jn:text-xs + jn:bg-theme-status-details + jn:text-theme-status-details + jn:py-0.5 + jn:px-1 +` + export const Status = ({ status = "error", title, @@ -85,13 +105,20 @@ export const Status = ({ const resolvedSpinner = spinner ?? status === "progress" return ( -
- {/* HTTP error codes are not rendered inside a DataGrid */} +
+ {/* suppress rendering HTTP error codes inside a DataGrid: */} {code && !isDataGrid &&
{code}
} {resolvedSpinner && } {resolvedTitle &&
{resolvedTitle}
} {resolvedBody &&
{resolvedBody}
} - {details &&
{details}
} + {details && ( +
+          {details}
+        
+ )} {action &&
{action}
}
) diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx index 56263ce6bc..fb323abba3 100644 --- a/packages/ui-components/src/components/Status/Status.stories.tsx +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -34,6 +34,37 @@ const meta: Meta = { export default meta type Story = StoryObj +const longStackTrace = `Error: Failed to fetch resource + at fetchData (api.ts:42) + at async loadServices (services.ts:17) + at async ServiceList.componentDidMount (ServiceList.tsx:88) + at async Promise.all (index 0) + at async fetchAll (dataLoader.ts:130) + at async DataLoader.load (dataLoader.ts:98) + at async DataLoader.reload (dataLoader.ts:112) + at async AppBootstrap.init (AppBootstrap.ts:54) + at async AppBootstrap.run (AppBootstrap.ts:67) + at async main (index.ts:12) +Caused by: NetworkError: net::ERR_CONNECTION_REFUSED + at XMLHttpRequest.onload (http.ts:23) + at XMLHttpRequest.dispatchEvent (xhr-polyfill.js:14) + at EventTarget.dispatchEvent (event-target.js:88) + at XMLHttpRequest.send (xhr.ts:201) + at HttpClient.request (http-client.ts:77) + at HttpClient.get (http-client.ts:92) + at fetchData (api.ts:38) + at retryWithBackoff (retry.ts:14) + at retryWithBackoff (retry.ts:22) + at retryWithBackoff (retry.ts:22) + at async fetchWithRetry (fetchWithRetry.ts:9) + at async ResourceStore.fetch (ResourceStore.ts:61) + at async ResourceStore.refresh (ResourceStore.ts:74) + at async ResourceStore.initialize (ResourceStore.ts:88) + at async App.bootstrap (App.tsx:33) + at async App.componentDidMount (App.tsx:44) + at async renderWithHooks (react-dom.development.js:14985) + at async commitLifeCycles (react-dom.development.js:20663)` + export const Default: Story = { args: {}, } @@ -52,7 +83,7 @@ export const HttpError: Story = { export const WithDetails: Story = { args: { status: "error", - details: `Error: Failed to fetch resource\n at fetchData (api.ts:42)\n at async loadServices (services.ts:17)`, + details: longStackTrace, }, parameters: { docs: { @@ -170,3 +201,32 @@ export const DataGridNoMatches: Story = { }, }, } + +export const DataGridErrorWithDetails: Story = { + args: { + status: "error", + details: longStackTrace, + }, + render: (args) => ( + + + Name + Region + Status + Last Updated + + + + + + + + ), + parameters: { + docs: { + description: { + story: "DataGrid error state with a stack trace passed via `details`.", + }, + }, + }, +} diff --git a/packages/ui-components/src/components/Status/Status.test.tsx b/packages/ui-components/src/components/Status/Status.test.tsx index 26273e4f98..2ad4d9df69 100644 --- a/packages/ui-components/src/components/Status/Status.test.tsx +++ b/packages/ui-components/src/components/Status/Status.test.tsx @@ -6,6 +6,9 @@ import React from "react" import { render, screen } from "@testing-library/react" import { Status } from "./Status.component" +import { DataGrid } from "../DataGrid/DataGrid.component" +import { DataGridRow } from "../DataGridRow/DataGridRow.component" +import { DataGridCell } from "../DataGridCell/DataGridCell.component" describe("Status", () => { it("renders a Status", () => { @@ -39,4 +42,30 @@ describe("Status", () => { render() expect(screen.getByRole("progressbar")).toHaveClass("jn:text-theme-accent") }) + + it("does not render a code inside a DataGrid", () => { + render( + + + + + + + + ) + expect(screen.queryByText("404")).not.toBeInTheDocument() + }) + + it("adds a datagrid identifier class when rendered inside a DataGrid", () => { + render( + + + + + + + + ) + expect(document.querySelector(".juno-status")).toHaveClass("juno-status-datagrid") + }) }) diff --git a/packages/ui-components/src/global.css b/packages/ui-components/src/global.css index 33d800a027..3b0686189e 100644 --- a/packages/ui-components/src/global.css +++ b/packages/ui-components/src/global.css @@ -381,6 +381,7 @@ --background-color-theme-required: var(--color-required-bg); --background-color-theme-select: var(--color-select-bg); --background-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active-bg); + --background-color-theme-status-details: var(--color-status-details-bg); --background-color-theme-switch-handle: var(--color-switch-handle-bg); --background-color-theme-switch-handle-checked: var(--color-switch-handle-checked-bg); --background-color-theme-tab-navigation-top: var(--color-tabnavigation-top-bg); @@ -424,6 +425,7 @@ --text-color-theme-pageheader-appname-default: var(--color-pageheader-appname-text-default); --text-color-theme-pageheader-appname-hover: var(--color-pageheader-appname-text-hover); --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active); + --text-color-theme-status-details: var(--color-status-details-text); /* Component Border Colors: */ --border-color-theme-default: var(--color-border-default); @@ -726,6 +728,9 @@ --color-codeblock-bg: var(--color-background-lvl-2); --color-codeblock-bar-border: var(--color-background-lvl-4); --color-codeblock-footer-border: var(--color-background-lvl-4); + /* LT Status */ + --color-status-details-bg: var(--color-background-lvl-2); + --color-status-details-text: var(--color-text-default); /* LT Panel */ --color-panel-bg: rgb(252 252 252 / 0.8); /* LT PageHeader */ @@ -986,6 +991,9 @@ --color-codeblock-bg: var(--color-background-lvl-2); --color-codeblock-bar-border: var(--color-background-lvl-4); --color-codeblock-footer-border: var(--color-background-lvl-4); + /* DT Status */ + --color-status-details-bg: var(--color-background-lvl-2); + --color-status-details-text: var(--color-text-default); /* DT Panel */ --color-panel-bg: rgb(from var(--color-juno-grey-blue-11) r g b / 0.75); /* DT PageHeader */ diff --git a/packages/ui-components/src/theme.css b/packages/ui-components/src/theme.css index f8484c08e1..02bcdc8efe 100644 --- a/packages/ui-components/src/theme.css +++ b/packages/ui-components/src/theme.css @@ -349,6 +349,7 @@ --background-color-theme-checkbox: var(--color-checkbox-bg); /* -> .jn:bg-theme-checkbox */ --background-color-theme-code-block: var(--color-codeblock-bg); + --background-color-theme-status-details: var(--color-status-details-bg); --background-color-theme-content-area-bg: var(--color-content-area-bg); @@ -407,6 +408,7 @@ --text-color-theme-pageheader-appname-default: var(--color-pageheader-appname-text-default); --text-color-theme-pageheader-appname-hover: var(--color-pageheader-appname-text-hover); --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active); + --text-color-theme-status-details: var(--color-status-details-text); /* Component Border Colors: */ --border-color-theme-default: var(--color-border-default); @@ -701,7 +703,9 @@ --color-codeblock-bg: var(--color-background-lvl-2); --color-codeblock-bar-border: var(--color-background-lvl-4); --color-codeblock-footer-border: var(--color-background-lvl-4); - /* LT Panel */ + /* LT Status */ + --color-status-details-bg: var(--color-background-lvl-2); + --color-status-details-text: var(--color-text-default); --color-panel-bg: rgb(252 252 252 / 0.8); /* LT PageHeader */ --color-pageheader-background: var(--color-juno-grey-blue-11); @@ -975,6 +979,9 @@ --color-codeblock-bg: var(--color-background-lvl-2); --color-codeblock-bar-border: var(--color-background-lvl-4); --color-codeblock-footer-border: var(--color-background-lvl-4); + /* DT Status */ + --color-status-details-bg: var(--color-background-lvl-2); + --color-status-details-text: var(--color-text-default); /* DT Panel */ --color-panel-bg: rgb(from var(--color-juno-grey-blue-11) r g b / 0.75); /* DT PageHeader */ From 2e815d92e7c83d0af573b470c4106bcfeedf47c2 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 21:37:05 +0200 Subject: [PATCH 09/20] feat(ui): add more styles, update default copy Signed-off-by: Franz Heidl --- .../components/Status/Status.component.tsx | 27 ++++++++++++++++--- packages/ui-components/src/global.css | 6 +++++ packages/ui-components/src/theme.css | 7 +++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx index c3c7bc6ba4..6d32ed9ad7 100644 --- a/packages/ui-components/src/components/Status/Status.component.tsx +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -26,8 +26,8 @@ const HTTP_ERRORS: Record = { const STATUS_DEFAULTS: Record = { progress: { title: "Loading…", body: "" }, error: { title: "Something went wrong", body: "An error occurred. Try again." }, - empty: { title: "No items", body: "" }, - "no-matches": { title: "No results", body: "No items match your current filters." }, + 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." }, } export interface StatusProps { @@ -60,6 +60,7 @@ const statusDataGridStyles = ` jn:min-h-[12.5rem] jn:max-h-[18.1875rem] jn:justify-center + jn:my-2 ` const detailsDataGridStyles = ` @@ -71,6 +72,18 @@ const codeStyles = ` jn:text-[12.5rem] jn:font-bold jn:leading-none + jn:text-theme-status-code +` + +const titleStyles = ` + jn:text-lg + jn:leading-[1.5] + jn:max-w-[50rem] +` + +const bodyStyles = ` + jn:leading-[1.5] + jn:max-w-[50rem] ` const detailsStyles = ` @@ -78,8 +91,14 @@ const detailsStyles = ` 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 ` export const Status = ({ @@ -112,8 +131,8 @@ export const Status = ({ {/* suppress rendering HTTP error codes inside a DataGrid: */} {code && !isDataGrid &&
{code}
} {resolvedSpinner && } - {resolvedTitle &&
{resolvedTitle}
} - {resolvedBody &&
{resolvedBody}
} + {resolvedTitle && {resolvedTitle}} + {resolvedBody &&
{resolvedBody}
} {details && (
           {details}
diff --git a/packages/ui-components/src/global.css b/packages/ui-components/src/global.css
index 3b0686189e..e425fe5035 100644
--- a/packages/ui-components/src/global.css
+++ b/packages/ui-components/src/global.css
@@ -426,6 +426,7 @@
   --text-color-theme-pageheader-appname-hover: var(--color-pageheader-appname-text-hover);
   --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active);
   --text-color-theme-status-details: var(--color-status-details-text);
+  --text-color-theme-status-code: var(--color-status-code);
 
   /* Component Border Colors: */
   --border-color-theme-default: var(--color-border-default);
@@ -469,6 +470,7 @@
   --border-color-theme-textinput-default: var(--color-textinput-default-border);
 
   --border-color-theme-sidenav: var(--color-sidenav-border);
+  --border-color-theme-status-details: var(--color-status-details-border);
 
   --padding-xs: 0.25rem;
   --padding-sm: 0.5rem;
@@ -731,6 +733,8 @@
   /* LT Status */
   --color-status-details-bg: var(--color-background-lvl-2);
   --color-status-details-text: var(--color-text-default);
+  --color-status-details-border: var(--color-border-default);
+  --color-status-code: var(--color-text-disabled);
   /* LT Panel */
   --color-panel-bg: rgb(252 252 252 / 0.8);
   /* LT PageHeader */
@@ -994,6 +998,8 @@
   /* DT Status */
   --color-status-details-bg: var(--color-background-lvl-2);
   --color-status-details-text: var(--color-text-default);
+  --color-status-details-border: var(--color-border-default);
+  --color-status-code: var(--color-text-disabled);
   /* DT Panel */
   --color-panel-bg: rgb(from var(--color-juno-grey-blue-11) r g b / 0.75);
   /* DT PageHeader */
diff --git a/packages/ui-components/src/theme.css b/packages/ui-components/src/theme.css
index 02bcdc8efe..d5308e0dd3 100644
--- a/packages/ui-components/src/theme.css
+++ b/packages/ui-components/src/theme.css
@@ -409,6 +409,7 @@
   --text-color-theme-pageheader-appname-hover: var(--color-pageheader-appname-text-hover);
   --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active);
   --text-color-theme-status-details: var(--color-status-details-text);
+  --text-color-theme-status-code: var(--color-status-code);
 
   /* Component Border Colors: */
   --border-color-theme-default: var(--color-border-default);
@@ -457,6 +458,7 @@
   --border-color-theme-textinput-default: var(--color-textinput-default-border);
 
   --border-color-theme-sidenav: var(--color-sidenav-border);
+  --border-color-theme-status-details: var(--color-status-details-border);
 
   --padding-xs: 0.25rem;
   --padding-sm: 0.5rem;
@@ -706,6 +708,9 @@
   /* LT Status */
   --color-status-details-bg: var(--color-background-lvl-2);
   --color-status-details-text: var(--color-text-default);
+  --color-status-details-border: var(--color-border-default);
+  --color-status-code: var(--color-text-disabled);
+  /* LT Panel */
   --color-panel-bg: rgb(252 252 252 / 0.8);
   /* LT PageHeader */
   --color-pageheader-background: var(--color-juno-grey-blue-11);
@@ -982,6 +987,8 @@
   /* DT Status */
   --color-status-details-bg: var(--color-background-lvl-2);
   --color-status-details-text: var(--color-text-default);
+  --color-status-details-border: var(--color-border-default);
+  --color-status-code: var(--color-text-disabled);
   /* DT Panel */
   --color-panel-bg: rgb(from var(--color-juno-grey-blue-11) r g b / 0.75);
   /* DT PageHeader */

From 0161859c35dc6bdafc4b48afaf131fce8f3dab3f Mon Sep 17 00:00:00 2001
From: Franz Heidl 
Date: Thu, 30 Jul 2026 21:51:20 +0200
Subject: [PATCH 10/20] feat(ui): add action story

Signed-off-by: Franz Heidl 
---
 .../src/components/Status/Status.component.tsx      |  2 +-
 .../src/components/Status/Status.stories.tsx        | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx
index 6d32ed9ad7..1212e773b1 100644
--- a/packages/ui-components/src/components/Status/Status.component.tsx
+++ b/packages/ui-components/src/components/Status/Status.component.tsx
@@ -138,7 +138,7 @@ export const Status = ({
           {details}
         
)} - {action &&
{action}
} + {action &&
{action}
}
) } diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx index fb323abba3..538dd9f6f3 100644 --- a/packages/ui-components/src/components/Status/Status.stories.tsx +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -6,6 +6,7 @@ import React from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { Status } from "./index" +import { Button } from "../Button/Button.component" import { DataGrid } from "../DataGrid/DataGrid.component" import { DataGridRow } from "../DataGridRow/DataGridRow.component" import { DataGridCell } from "../DataGridCell/DataGridCell.component" @@ -95,6 +96,18 @@ export const WithDetails: Story = { }, } +export const WithAction: Story = { + args: { status: "error" }, + render: (args) => Retry} />, + parameters: { + docs: { + description: { + story: "Pass any element via the `action` slot — typically a `Button` or a button-styled anchor.", + }, + }, + }, +} + export const DataGridLoading: Story = { args: { status: "progress" }, render: (args) => ( From 4eb9c7f30cf99df0a44d118a8e697566a684397a Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 21:52:40 +0200 Subject: [PATCH 11/20] feat(ui): add more tests Signed-off-by: Franz Heidl --- .../src/components/Status/Status.test.tsx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/ui-components/src/components/Status/Status.test.tsx b/packages/ui-components/src/components/Status/Status.test.tsx index 2ad4d9df69..4fe06f15e1 100644 --- a/packages/ui-components/src/components/Status/Status.test.tsx +++ b/packages/ui-components/src/components/Status/Status.test.tsx @@ -43,6 +43,68 @@ describe("Status", () => { expect(screen.getByRole("progressbar")).toHaveClass("jn:text-theme-accent") }) + it("renders the default error title with no props", () => { + render() + expect(screen.getByText("Something went wrong")).toBeInTheDocument() + }) + + it("renders the default title for the progress status", () => { + render() + expect(screen.getByText("Loading…")).toBeInTheDocument() + }) + + it("renders the default title for the empty status", () => { + render() + expect(screen.getByText("No items")).toBeInTheDocument() + }) + + it("renders title and body from HTTP error code", () => { + render() + expect(screen.getByText("Page Not Found")).toBeInTheDocument() + expect(screen.getByText("The requested URL does not exist or may have moved.")).toBeInTheDocument() + }) + + it("overrides default title when title prop is passed", () => { + render() + expect(screen.getByText("Custom title")).toBeInTheDocument() + }) + + it("renders body text", () => { + render() + expect(screen.getByText("Please try again.")).toBeInTheDocument() + }) + + it("renders details in a pre element", () => { + render() + expect(document.querySelector(".juno-status-details")).toBeInTheDocument() + expect(screen.getByText("Error: something failed")).toBeInTheDocument() + }) + + it("does not render details when not passed", () => { + render() + expect(document.querySelector(".juno-status-details")).not.toBeInTheDocument() + }) + + it("renders code outside a DataGrid", () => { + render() + expect(screen.getByText("500")).toBeInTheDocument() + }) + + it("renders the action slot", () => { + render(Retry} />) + expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument() + }) + + it("renders a custom className", () => { + render() + expect(document.querySelector(".juno-status")).toHaveClass("my-custom-class") + }) + + it("passes arbitrary props to the root element", () => { + render() + expect(screen.getByTestId("my-status")).toBeInTheDocument() + }) + it("does not render a code inside a DataGrid", () => { render( From 04b77e3cfd119ac89202679728dd30a5c5ff6dbd Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 21:57:55 +0200 Subject: [PATCH 12/20] feat(ui): improve accessibiltiy, improve tests Signed-off-by: Franz Heidl --- .../components/Status/Status.component.tsx | 10 +++++-- .../src/components/Status/Status.test.tsx | 26 +++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx index 1212e773b1..27a5e313e7 100644 --- a/packages/ui-components/src/components/Status/Status.component.tsx +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -123,18 +123,24 @@ export const Status = ({ const resolvedSpinner = spinner ?? status === "progress" + const role = status === "error" ? "alert" : "status" + return (
{/* suppress rendering HTTP error codes inside a DataGrid: */} {code && !isDataGrid &&
{code}
} - {resolvedSpinner && } + {resolvedSpinner && } {resolvedTitle && {resolvedTitle}} {resolvedBody &&
{resolvedBody}
} {details && ( -
+        
           {details}
         
)} diff --git a/packages/ui-components/src/components/Status/Status.test.tsx b/packages/ui-components/src/components/Status/Status.test.tsx index 4fe06f15e1..c797c1f329 100644 --- a/packages/ui-components/src/components/Status/Status.test.tsx +++ b/packages/ui-components/src/components/Status/Status.test.tsx @@ -13,7 +13,17 @@ import { DataGridCell } from "../DataGridCell/DataGridCell.component" describe("Status", () => { it("renders a Status", () => { render() - expect(document.querySelector(".juno-status")).toBeInTheDocument() + expect(screen.getByRole("alert")).toBeInTheDocument() + }) + + it("renders with role alert for error status", () => { + render() + expect(screen.getByRole("alert")).toBeInTheDocument() + }) + + it("renders with role status for non-error statuses", () => { + render() + expect(screen.getByRole("status")).toBeInTheDocument() }) it("renders a spinner when status is progress", () => { @@ -45,17 +55,17 @@ describe("Status", () => { it("renders the default error title with no props", () => { render() - expect(screen.getByText("Something went wrong")).toBeInTheDocument() + expect(screen.getByRole("alert")).toHaveTextContent("Something went wrong") }) it("renders the default title for the progress status", () => { render() - expect(screen.getByText("Loading…")).toBeInTheDocument() + expect(screen.getByRole("status")).toHaveTextContent("Loading…") }) it("renders the default title for the empty status", () => { render() - expect(screen.getByText("No items")).toBeInTheDocument() + expect(screen.getByRole("status")).toHaveTextContent("No items") }) it("renders title and body from HTTP error code", () => { @@ -76,13 +86,13 @@ describe("Status", () => { it("renders details in a pre element", () => { render() - expect(document.querySelector(".juno-status-details")).toBeInTheDocument() + expect(screen.getByLabelText("Error details")).toBeInTheDocument() expect(screen.getByText("Error: something failed")).toBeInTheDocument() }) it("does not render details when not passed", () => { render() - expect(document.querySelector(".juno-status-details")).not.toBeInTheDocument() + expect(screen.queryByLabelText("Error details")).not.toBeInTheDocument() }) it("renders code outside a DataGrid", () => { @@ -97,7 +107,7 @@ describe("Status", () => { it("renders a custom className", () => { render() - expect(document.querySelector(".juno-status")).toHaveClass("my-custom-class") + expect(screen.getByRole("alert")).toHaveClass("my-custom-class") }) it("passes arbitrary props to the root element", () => { @@ -128,6 +138,6 @@ describe("Status", () => { ) - expect(document.querySelector(".juno-status")).toHaveClass("juno-status-datagrid") + expect(screen.getByRole("alert")).toHaveClass("juno-status-datagrid") }) }) From 4430779d8cf47976c79db0ca67add41cd3e8e4b4 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 22:15:32 +0200 Subject: [PATCH 13/20] Create deep-jobs-rush.md Signed-off-by: Franz Heidl --- .changeset/deep-jobs-rush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/deep-jobs-rush.md diff --git a/.changeset/deep-jobs-rush.md b/.changeset/deep-jobs-rush.md new file mode 100644 index 0000000000..4abb91822d --- /dev/null +++ b/.changeset/deep-jobs-rush.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-ui-components": patch +--- + +feat(ui): add `Status` component From f3c90c67af34c90adda120e8470603c8122dc62b Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Thu, 30 Jul 2026 22:10:45 +0200 Subject: [PATCH 14/20] feat(ui): add 400 error, improve props, Signed-off-by: Franz Heidl --- .../ui-components/src/components/Status/Status.component.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx index 27a5e313e7..3abb1bec71 100644 --- a/packages/ui-components/src/components/Status/Status.component.tsx +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -8,6 +8,7 @@ import { Spinner } from "../Spinner" import { useDataGridContext } from "../DataGrid/DataGrid.component" const HTTP_ERRORS: Record = { + 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." }, 404: { title: "Page Not Found", body: "The requested URL does not exist or may have moved." }, @@ -30,7 +31,7 @@ const STATUS_DEFAULTS: Record = { "no-matches": { title: "", body: "No items match the current filters. Adjust or clear filters." }, } -export interface StatusProps { +export interface StatusProps extends React.HTMLAttributes { /** 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. */ From 3bb13e88f51b5a0befa71e0e267574694eb27256 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Fri, 31 Jul 2026 10:27:29 +0200 Subject: [PATCH 15/20] feat(ui): add Status reference to ux docs, remove TODOs Signed-off-by: Franz Heidl --- docs/ux/error-handling-loading-empty-states.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/ux/error-handling-loading-empty-states.md b/docs/ux/error-handling-loading-empty-states.md index 3db5b0d67d..a9dd14a5d4 100644 --- a/docs/ux/error-handling-loading-empty-states.md +++ b/docs/ux/error-handling-loading-empty-states.md @@ -2,6 +2,9 @@ # Error And Empty/Loading/Busy State Handling Strategies +> [!NOTE] +> For all error, loading, empty, and no-matches states that do not have a dedicated local or component-specific solution, use the [`Status`](https://cloudoperators.github.io/juno/?path=/docs/components-status--docs) component. It covers application-, page-, section-, and DataGrid-level states, ships with sensible defaults for all cases described in this document, and is the required default for any situation not handled by a more specific pattern. Full API and usage examples are in Storybook. + # Error State Handling Patterns and rules for handling errors consistently across our applications. @@ -63,10 +66,6 @@ A single component fails to render and/or does not receive the expected API data **See the [HTTP Error Code Reference](#http-error-code-reference) for specific codes, titles, and messages.** -> **TODO:** Identify components prone to such errors, design and implement loading, empty, error states. (A good and most urgent candidate is DataGrid.) - -> **TODO:** Identify and design error and empty states for affected components. - ## Operation/Action/CRUD errors A user has initiated an action, such as creating, updating, or deleting an item or entity, and the operation fails immediately or with some delay. From fe5cd6a94f887bc6b026b39525c22fba89adeade Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Fri, 31 Jul 2026 10:52:28 +0200 Subject: [PATCH 16/20] feat(ui): add DataGrid headers to DataGrid stories Signed-off-by: Franz Heidl --- .../src/components/Status/Status.stories.tsx | 173 +++++++++++------- 1 file changed, 106 insertions(+), 67 deletions(-) diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx index 538dd9f6f3..4131071004 100644 --- a/packages/ui-components/src/components/Status/Status.stories.tsx +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -11,6 +11,9 @@ import { DataGrid } from "../DataGrid/DataGrid.component" import { DataGridRow } from "../DataGridRow/DataGridRow.component" import { DataGridCell } from "../DataGridCell/DataGridCell.component" import { DataGridHeadCell } from "../DataGridHeadCell/DataGridHeadCell.component" +import { DataGridToolbar } from "../DataGridToolbar/DataGridToolbar.component" +import { SearchInput } from "../SearchInput/SearchInput.component" +import { Stack } from "../Stack/Stack.component" const meta: Meta = { title: "Components/Status", @@ -109,26 +112,34 @@ export const WithAction: Story = { } export const DataGridLoading: Story = { - args: { status: "progress" }, + args: { status: "progress", title: "Loading Servers …" }, render: (args) => ( - - - Name - Region - Status - Last Updated - - - - - - - + + + + + + + + + Name + Region + Status + Last Updated + + + + + + + + ), parameters: { docs: { description: { - story: 'Use `status="progress"` inside a `DataGridRow` spanning all columns while data is being fetched.', + story: + 'Use `status="progress"` inside a `DataGridRow` spanning all columns while data is being fetched. Use the title `title` prop to further qualify the kind of items currently being loaded if possible, otherwise the default title "Loading …" will be rendered.', }, }, }, @@ -137,19 +148,26 @@ export const DataGridLoading: Story = { export const DataGridError: Story = { args: { status: "error" }, render: (args) => ( - - - Name - Region - Status - Last Updated - - - - - - - + + + + + + + + + Name + Region + Status + Last Updated + + + + + + + + ), parameters: { docs: { @@ -164,19 +182,26 @@ export const DataGridError: Story = { export const DataGridEmpty: Story = { args: { status: "empty" }, render: (args) => ( - - - Name - Region - Status - Last Updated - - - - - - - + + + + + + + + + Name + Region + Status + Last Updated + + + + + + + + ), parameters: { docs: { @@ -191,19 +216,26 @@ export const DataGridEmpty: Story = { export const DataGridNoMatches: Story = { args: { status: "no-matches" }, render: (args) => ( - - - Name - Region - Status - Last Updated - - - - - - - + + + + + + + + + Name + Region + Status + Last Updated + + + + + + + + ), parameters: { docs: { @@ -221,19 +253,26 @@ export const DataGridErrorWithDetails: Story = { details: longStackTrace, }, render: (args) => ( - - - Name - Region - Status - Last Updated - - - - - - - + + + + + + + + + Name + Region + Status + Last Updated + + + + + + + + ), parameters: { docs: { From c754e823aa0751d8e6d5266311d41d01ca3ac2ac Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Fri, 31 Jul 2026 10:56:07 +0200 Subject: [PATCH 17/20] feat(ui): add standalone Loading story Signed-off-by: Franz Heidl --- .../src/components/Status/Status.stories.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx index 4131071004..2b8fa3c628 100644 --- a/packages/ui-components/src/components/Status/Status.stories.tsx +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -111,6 +111,17 @@ export const WithAction: Story = { }, } +export const Loading: Story = { + args: { status: "progress" }, + parameters: { + docs: { + description: { + story: 'Use `status="progress"` at page or section level while data is being fetched.', + }, + }, + }, +} + export const DataGridLoading: Story = { args: { status: "progress", title: "Loading Servers …" }, render: (args) => ( From befbf560c16c884e51a28de5a2d643c5e2233f15 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Fri, 31 Jul 2026 11:52:54 +0200 Subject: [PATCH 18/20] feat(ui): add reference to Status comonent to DataGrid docs Signed-off-by: Franz Heidl --- docs/ux/datagrid.md | 4 ++++ .../ux/images/status-generic-error-datagrid.png | Bin 0 -> 62567 bytes 2 files changed, 4 insertions(+) create mode 100644 docs/ux/images/status-generic-error-datagrid.png diff --git a/docs/ux/datagrid.md b/docs/ux/datagrid.md index e7b12a38b4..eb0021d225 100644 --- a/docs/ux/datagrid.md +++ b/docs/ux/datagrid.md @@ -104,6 +104,10 @@ A DataGrid must handle the full range of data states gracefully. Do not render a - **Empty (filtered):** Distinguish between "no data exists" and "no data matches your filters" — these suggest different user actions - **Error:** Show an error state scoped to the DataGrid with a clear explanation and a retry option where possible. If present, refer users to the Reload/Refresh button described below. +Use the [`Status`](https://cloudoperators.github.io/juno/?path=/docs/components-status--docs) component for all of these states. Place it inside a `DataGridRow` and `DataGridCell` spanning all columns. `Status` automatically adapts its styling to the DataGrid context. See Storybook for usage examples. + +![A DataGrid with a generic error rendered using the `Status` compponent](images/status-generic-error-datagrid.png) + ## Re-Loading Data A DataGrid may have a Reload/Refresh-button in the Header in order to make sure the latest state of the data is displayed and interacted with. diff --git a/docs/ux/images/status-generic-error-datagrid.png b/docs/ux/images/status-generic-error-datagrid.png new file mode 100644 index 0000000000000000000000000000000000000000..86b8edf6282d1a53f0f8dc51fc2443958aaabf54 GIT binary patch literal 62567 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�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 literal 0 HcmV?d00001 From f31c9372a3905677e4dca933d5d48a8eb9d9f69d Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Fri, 31 Jul 2026 11:54:52 +0200 Subject: [PATCH 19/20] feat(ui): set correct year 2026 for copyright notices Signed-off-by: Franz Heidl --- .../ui-components/src/components/Status/Status.component.tsx | 2 +- packages/ui-components/src/components/Status/Status.stories.tsx | 2 +- packages/ui-components/src/components/Status/Status.test.tsx | 2 +- packages/ui-components/src/components/Status/index.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx index 3abb1bec71..b1caf748e2 100644 --- a/packages/ui-components/src/components/Status/Status.component.tsx +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx index 2b8fa3c628..60b951b2ca 100644 --- a/packages/ui-components/src/components/Status/Status.stories.tsx +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/ui-components/src/components/Status/Status.test.tsx b/packages/ui-components/src/components/Status/Status.test.tsx index c797c1f329..d4e75723db 100644 --- a/packages/ui-components/src/components/Status/Status.test.tsx +++ b/packages/ui-components/src/components/Status/Status.test.tsx @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/ui-components/src/components/Status/index.ts b/packages/ui-components/src/components/Status/index.ts index 07333cdedc..57cb34beca 100644 --- a/packages/ui-components/src/components/Status/index.ts +++ b/packages/ui-components/src/components/Status/index.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors * SPDX-License-Identifier: Apache-2.0 */ From bcf227f0b80a5a650c6af23cf282327921964953 Mon Sep 17 00:00:00 2001 From: Franz Heidl Date: Fri, 31 Jul 2026 15:42:31 +0200 Subject: [PATCH 20/20] feat(ui): add simple Status in page context story to be refined Signed-off-by: Franz Heidl --- .../src/components/Status/Status.stories.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx index 60b951b2ca..1041a4e134 100644 --- a/packages/ui-components/src/components/Status/Status.stories.tsx +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -14,6 +14,7 @@ import { DataGridHeadCell } from "../DataGridHeadCell/DataGridHeadCell.component import { DataGridToolbar } from "../DataGridToolbar/DataGridToolbar.component" import { SearchInput } from "../SearchInput/SearchInput.component" import { Stack } from "../Stack/Stack.component" +import { PageHeader } from "../PageHeader/PageHeader.component" const meta: Meta = { title: "Components/Status", @@ -38,6 +39,13 @@ const meta: Meta = { export default meta type Story = StoryObj +const MockPageContextDecorator = (Story: React.ComponentType) => ( +
+ + +
+) + const longStackTrace = `Error: Failed to fetch resource at fetchData (api.ts:42) at async loadServices (services.ts:17) @@ -122,6 +130,11 @@ export const Loading: Story = { }, } +export const InPageContext: Story = { + decorators: [MockPageContextDecorator], + args: { className: "jn:mt-8" }, +} + export const DataGridLoading: Story = { args: { status: "progress", title: "Loading Servers …" }, render: (args) => (