Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add statistical card to extensibility #2962

Merged
merged 22 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 57 additions & 38 deletions docs/extensibility/30-details-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ You can customize the details page of the user interface component of your resou

## Available Parameters

In the **data.details** section you can provide configuration of four optional components: **header**, **body**, **status** and **resourceGraph**. The **header**, **status** and **body** components are lists of widgets visible in the respective sections of the details page. You can use the **resourceGraph** component to present the relationship between different resources.
In the **data.details** section you can provide configuration of four optional components: **header**, **body**, **status**, **health** and **resourceGraph**. The **header**, **status**, **body** and **health** components are lists of widgets visible in the respective sections of the details page. You can use the **resourceGraph** component to present the relationship between different resources.

### **header** , **status** and **body** Parameters
### **header**, **status**, **body** and **health** Parameters

This table lists the available parameters of the **data.details.header**, **data.details.status** and/or **data.details.body** section in your resource ConfigMap. You can learn whether each of the parameters is required and what purpose it serves. The **data.details.header** and **data.details.body** components are arrays of objects.
This table lists the available parameters of the **data.details.header**, **data.details.status**, **data.details.health** and/or **data.details.body** section in your resource ConfigMap. You can learn whether each of the parameters is required and what purpose it serves. The **data.details.header**, **data.details.status**, **data.details.health** and **data.details.body** components are arrays of objects.

| Parameter | Required | Type | Description |
| --------------------- | -------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -24,41 +24,60 @@ Extra parameters might be available for specific widgets.
See the following examples:

```yaml
header:
- source: metadata.name
- source: spec.priority
widget: Badge
- source: "$join(spec.volumes.name, ', ')"
body:
- name: columns
widget: Columns
children:
- name: left-panel
widget: Panel
- name: right-panel
widget: Panel
- name: summary
widget: Panel
children:
- source: metadata.name
- source: spec.priority
widget: Badge
- name: Volumes names of volumes with config map
source: "$join(spec.volumes['configMap' in $keys($)].name, ', ')"
- source: spec.details
widget: CodeViewer
language: "'json'"
- source: spec.configPatches
widget: Panel
children:
- source: applyTo
- source: match.context
visibility: '$exists($value.match.context)'
- source: spec.configPatches
widget: Table
children:
- source: applyTo
- source: match.context
details:
header:
- source: metadata.name
- source: spec.priority
widget: Badge
- source: "$join(spec.volumes.name, ', ')"
status:
- name: Replicas
source: status.replicas
- name: Condition details
widget: ConditionList
source: status.conditions
health:
- name: MyTitle
widget: StatisticalCard
source: status
mainValue:
name: MySubtitle
source: $item.importantValue
children:
- name: ExtraInformation1
source: $item.value1
- name: ExtraInformation2
source: $item.value2
body:
- name: columns
widget: Columns
children:
- name: left-panel
widget: Panel
- name: right-panel
widget: Panel
- name: summary
widget: Panel
children:
- source: metadata.name
- source: spec.priority
widget: Badge
- name: Volumes names of volumes with config map
source: "$join(spec.volumes['configMap' in $keys($)].name, ', ')"
- source: spec.details
widget: CodeViewer
language: "'json'"
- source: spec.configPatches
widget: Panel
children:
- source: applyTo
- source: match.context
visibility: '$exists($value.match.context)'
- source: spec.configPatches
widget: Table
children:
- source: applyTo
- source: match.context
```

### Data Scoping
Expand Down
74 changes: 67 additions & 7 deletions docs/extensibility/50-list-and-details-widgets.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# List and Details Widgets

You can use list and details widgets in the lists and details pages in the user interface component of your resource. You can distinguish the following widget types:
You can use list and details widgets in the lists and details pages in the user interface component of your resource.
You can distinguish the following widget types:

- [Inline widgets](#inline-widgets) for simple values in **data.list**, **data.details.header**, **data.details.status** and **data.detail.bodies**
- [`Bagde`](#badge)
Expand All @@ -23,6 +24,7 @@ You can use list and details widgets in the lists and details pages in the user
- [`Plain`](#plain)
- [`ResourceList`](#resourcelist)
- [`ResourceRefs`](#resourcerefs)
- [`StatisticalCard`](#StatisticalCard)
- [`Table`](#table)
- [`Tabs`](#tabs)

Expand Down Expand Up @@ -91,16 +93,17 @@ This is an exaple of kind only:
kindOnly: true
```

### ConditionList
### `ConditionList`

The condition List widget renders the conditions as an expandable list with condition details.
The `ConditionList` widget renders the conditions as an expandable list with condition details. This widget is primarily designed for the overview section **data.details.status**

#### Example
See the following example:

```yaml
- name: Condition details
widget: ConditionList
source: status.conditions
status:
- name: Condition details
widget: ConditionList
source: status.conditions
```

<img src="./assets/display-widgets/ConditionList.png" alt="Example of a condition list widget" style="border: 1px solid #D2D5D9">
Expand Down Expand Up @@ -578,6 +581,63 @@ See the following example:

<img src="./assets/display-widgets/ResourceRefs.png" alt="Example of a ResourceRefs widget" style="border: 1px solid #D2D5D9">

### `StatisticalCard`

`StatisticalCard` widgets render a card component with several numerical pieces of information.
To display the widget in the **Monitoring and Health** section of a details page, configure it in **data.details.health**.
To render the card within the dense grid layout in the **Monitoring and Health** section of **Cluster Details**, use [injections](#widget-injections-overview) (**destination: ClusterOverview, slot: health**).

These are the available `StatisticalCard` widget parameters:

| Parameter | Required | Type | Description |
| -------------------- | -------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **mainValue** | **Yes** | object | The main value displayed using a bigger font. |
| **mainValue.source** | **Yes** | string or [JSONata](jsonata.md) expression | Fetches data for the column. In its simplest form, it's the path to the value. |
| **mainValue.name** | **Yes** | string | The name for the primary label of this field. Required for most widgets (except for some rare cases that don't display a label). This can be a key to use from the [**translation** section](./translations-section.md). |
| **children** | No | array of objects | An array of additional values, listed next to the main one. |
| **children.source** | **Yes** | string or [JSONata](jsonata.md) expression | Fetches data for the column. In its simplest form, it's the path to the value. |
| **children.name** | **Yes** | string | The name for the primary label of this field. Required for most widgets (except for some rare cases that don't display a label). This can be a key to use from the [**translation** section](./translations-section.md). |

This is an example of the widget configuration in the **data.details.health** section which allows the `StatisticalCard` to be displayed on the details page in the **Monitoring and Health** section:

```yaml
details: |-
health:
- name: MyTitle
widget: StatisticalCard
source: status
mainValue:
name: MySubtitle
source: $item.importantValue
children:
- name: ExtraInformation1
source: $item.value1
- name: ExtraInformation2
source: $item.value2
```

This is an example of the widget configured using injection which allows the `StatisticalCard` to be displayed in the **Monitoring and Health** section of **Cluster Details**:

```yaml
injections: |-
- name: MyTitle
widget: StatisticalCard
source: status
mainValue:
name: MySubtitle
source: $sum($item.importantValue)
children:
- name: ExtraInformation1
source: $max($item.value1)
- name: ExtraInformation2
source: $count($item.value2)
targets:
- slot: health
location: ClusterOverview
```

<img src="./assets/display-widgets/StatisticalCard.png" alt="Example of a StatisticalCard widget" style="border: 1px solid #D2D5D9" width="75%">

### `Table`

Table widgets display array data as rows of a table instead of free-standing components. The **children** parameter defines the values used to render the columns. Similar to the **list** section of the ConfigMap, you should use inline widgets only as children.
Expand Down
1 change: 1 addition & 0 deletions docs/extensibility/70-widget-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ These are the available **injections** widget parameters:
- **details-header** - In the header of the details view
- **details-top** - At the top of the resource view
- **banner** - At the top of the resource view. This slot should be only used with `location: ClusterOverview` and [`widget: FeaturedCard`](./50-list-and-details-widgets.md#featuredcard).
- **health** - At the top of the resource view. This slot should be only used with `location: ClusterOverview` and [`widget: StatisticalCard`](./50-list-and-details-widgets.md#statisticalcard).
- **list-header** - In the header of the list view

## Available **injections** Locations
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ cluster-overview:
cluster-details: Cluster Details
metadata: Metadata
statistics:
title: Monitoring and Health
cpu-usage: 'CPU Usage'
memory-usage: 'Memory Usage'
namespaces-health: 'Namespaces Health'
Expand Down Expand Up @@ -327,6 +326,7 @@ common:
link: Link
loading: Loading...
logs: Logs
monitoring-and-health: Monitoring and Health
name: Name
namespace: Namespace
namespaces: Namespaces
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { spacing } from '@ui5/webcomponents-react-base';
import { useTranslation } from 'react-i18next';
import { UI5RadialChart } from 'shared/components/UI5RadialChart/UI5RadialChart';
Expand All @@ -17,6 +17,10 @@ import {
import { roundTwoDecimals } from 'shared/utils/helpers';
import './ClusterStats.scss';

const Injections = React.lazy(() =>
import('../../../Extensibility/ExtensibilityInjections'),
);

export default function ClusterStats({ nodesData }) {
const { t } = useTranslation();

Expand Down Expand Up @@ -99,7 +103,7 @@ export default function ClusterStats({ nodesData }) {
...spacing.sapUiMediumMarginTopBottom,
}}
>
{t('cluster-overview.statistics.title')}
{t('common.headers.monitoring-and-health')}
</Title>
<div className="cluster-stats" style={spacing.sapUiTinyMarginBeginEnd}>
<div className="item-wrapper tall">
Expand Down Expand Up @@ -282,6 +286,7 @@ export default function ClusterStats({ nodesData }) {
/>
</div>
)}
<Injections destination="ClusterOverview" slot="health" root="" />
</div>
</>
);
Expand Down
20 changes: 20 additions & 0 deletions src/components/Extensibility/ExtensibilityDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@ export const ExtensibilityDetailsCore = ({
}

const header = resMetaData?.details?.header || [];
const health = resMetaData?.details?.health || [];
const status = resMetaData?.details?.status || [];
const body = resMetaData?.details?.body || [];
const dataSources = resMetaData?.dataSources || {};
const general = resMetaData?.general || {};

return (
<ResourceDetails
Expand Down Expand Up @@ -158,6 +160,24 @@ export const ExtensibilityDetailsCore = ({
}))
: []
}
customHealthCards={
Array.isArray(health)
? [
(resource, i) => (
<Widget
key={i}
value={resource}
structure={health}
schema={schema}
dataSources={dataSources}
general={general}
originalResource={resource}
context={general.urlPath}
/>
),
]
: []
}
description={description}
createResourceForm={ExtensibilityCreate}
resourceSchema={resMetaData}
Expand Down
3 changes: 3 additions & 0 deletions src/components/Extensibility/ExtensibilityInjections.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const ExtensibilityInjectionCore = ({ resMetaData, root }) => {
}

const dataSources = resMetaData?.dataSources || {};
const general = resMetaData?.general || {};
const injection = resMetaData?.injection;
const injectionName = injection?.name;
const filter = injection?.target.filter || injection?.filter || null;
Expand All @@ -58,9 +59,11 @@ export const ExtensibilityInjectionCore = ({ resMetaData, root }) => {
structure={injection}
schema={schema}
dataSources={dataSources}
general={general}
originalResource={filteredItems}
embedResource={root}
inlineContext={true}
context={injection.target}
/>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/Extensibility/components/ConditionList.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ConditionList as ConditionListComponent } from 'shared/components/ConditionList/ConditionList';

export const ConditionList = ({ value, structure }) => {
export const ConditionList = ({ value }) => {
if (!Array.isArray(value) || value?.length === 0) {
return null;
}
Expand Down
60 changes: 60 additions & 0 deletions src/components/Extensibility/components/StatisticalCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { CountingCard } from 'shared/components/CountingCard/CountingCard';
import { useJsonata } from '../hooks/useJsonata';
import { useGetTranslation } from '../helpers';
import { EMPTY_TEXT_PLACEHOLDER } from 'shared/constants';

export function StatisticalCard({
structure,
value,
originalResource,
general,
context,
}) {
const jsonata = useJsonata({
resource: originalResource,
value,
});
const { t } = useGetTranslation();

const extraInfo = structure.children?.map(child => {
const [childValue, err] = jsonata(child.source, {
resource: value,
});
if (err) {
return t('extensibility.configuration-error', {
error: err.message,
});
}

return {
title: child.name,
value: childValue !== undefined ? childValue : EMPTY_TEXT_PLACEHOLDER,
};
});

const [mainValue, err] = jsonata(structure?.mainValue?.source, {
resource: value,
});
if (err) {
return t('extensibility.configuration-error', {
error: err.message,
});
}

return (
<div className={`item-wrapper ${extraInfo ? 'wide' : 'small'}`}>
<CountingCard
className="item"
value={mainValue !== undefined ? mainValue : EMPTY_TEXT_PLACEHOLDER}
title={structure?.name}
subTitle={structure?.mainValue?.name}
resourceUrl={context !== general?.urlPath ? general?.urlPath : null}
isClusterResource={general?.scope === 'cluster'}
allNamespaceURL={general?.scope === 'namespace'}
extraInfo={extraInfo}
/>
</div>
);
}

StatisticalCard.array = true;
Loading
Loading