Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba9e646
feat(ui): add Status component stub, stories, tests
franzheidl Jul 30, 2026
6061e59
feat(ui): always render blue spinner
franzheidl Jul 30, 2026
4604348
feat(ui): use generic error as default status
franzheidl Jul 30, 2026
05097ab
feat(ui): add Datagrid code to stories for example visibility
franzheidl Jul 30, 2026
15435ab
feat(ui): add some tests
franzheidl Jul 30, 2026
b752441
feat(ui): add DataGrid context variable
franzheidl Jul 30, 2026
3e7968b
feat(ui): add styles, don’t render code in DataGrid context
franzheidl Jul 30, 2026
a808c1e
feat(ui): add more styles and stories
franzheidl Jul 30, 2026
2e815d9
feat(ui): add more styles, update default copy
franzheidl Jul 30, 2026
0161859
feat(ui): add action story
franzheidl Jul 30, 2026
4eb9c7f
feat(ui): add more tests
franzheidl Jul 30, 2026
04b77e3
feat(ui): improve accessibiltiy, improve tests
franzheidl Jul 30, 2026
4430779
Create deep-jobs-rush.md
franzheidl Jul 30, 2026
f3c90c6
feat(ui): add 400 error, improve props,
franzheidl Jul 30, 2026
3bb13e8
feat(ui): add Status reference to ux docs, remove TODOs
franzheidl Jul 31, 2026
fe5cd6a
feat(ui): add DataGrid headers to DataGrid stories
franzheidl Jul 31, 2026
c754e82
feat(ui): add standalone Loading story
franzheidl Jul 31, 2026
befbf56
feat(ui): add reference to Status comonent to DataGrid docs
franzheidl Jul 31, 2026
f31c937
feat(ui): set correct year 2026 for copyright notices
franzheidl Jul 31, 2026
bcf227f
feat(ui): add simple Status in page context story
franzheidl Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const gridTemplate = (

interface DataGridContextType {
cellVerticalAlignment?: CellVerticalAlignmentType
isDataGrid?: boolean
}

const DataGridContext = createContext<DataGridContextType>({})
Expand All @@ -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 (
Expand Down
150 changes: 150 additions & 0 deletions packages/ui-components/src/components/Status/Status.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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"
import { useDataGridContext } from "../DataGrid/DataGrid.component"

const HTTP_ERRORS: Record<number, { title: string; body: string }> = {
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." },
Comment thread
franzheidl marked this conversation as resolved.
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<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." },
}

export interface StatusProps {
Comment thread
franzheidl marked this conversation as resolved.
Outdated
/** 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
/** 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 `<pre>` 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
}

const statusStyles = `
jn:flex
jn:flex-col
jn:items-center
jn:text-center
`

const statusDataGridStyles = `
jn:min-h-[12.5rem]
jn:max-h-[18.1875rem]
jn:justify-center
jn:my-2
`

const detailsDataGridStyles = `
jn:min-h-0
jn:overflow-y-auto
`

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 = `
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
`

export const Status = ({
status = "error",
title,
body,
spinner,
code,
details,
action,
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

const resolvedTitle = title ?? httpDefaults?.title ?? statusDefaults?.title
const resolvedBody = body ?? httpDefaults?.body ?? statusDefaults?.body

const resolvedSpinner = spinner ?? status === "progress"

const role = status === "error" ? "alert" : "status"

return (
<div
role={role}
className={`juno-status ${isDataGrid ? `juno-status-datagrid ${statusDataGridStyles}` : ""} ${statusStyles} ${className}`}
{...props}
>
{/* suppress rendering HTTP error codes inside a DataGrid: */}
{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>}
{resolvedBody && <div className={`juno-status-body ${bodyStyles}`}>{resolvedBody}</div>}
{details && (
<pre
aria-label="Error details"
className={`juno-status-details ${detailsStyles} ${isDataGrid ? detailsDataGridStyles : ""}`}
>
{details}
</pre>
)}
{action && <div className="juno-status-action jn:mt-4">{action}</div>}
</div>
)
}
Loading
Loading