diff --git a/.github/workflows/accessibility-tests.yml b/.github/workflows/accessibility-tests.yml index 78b66b255f..e0f30e0a59 100644 --- a/.github/workflows/accessibility-tests.yml +++ b/.github/workflows/accessibility-tests.yml @@ -13,11 +13,9 @@ on: jobs: run-accessibility-tests: runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - name: Install k3d env: K3D_URL: https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh @@ -29,30 +27,17 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - - name: setup_busola + - name: Setup busola shell: bash run: | - set -e - npm ci - npm run build - npm i -g serve - - name: run_tests + .github/scripts/setup_local_busola.sh + - name: Run tests shell: bash env: ACC_AMP_TOKEN: ${{ secrets.ACC_AMP_TOKEN }} run: | - k3d kubeconfig get kyma > tests/integration/fixtures/kubeconfig.yaml - export CYPRESS_DOMAIN=http://localhost:3000 - serve -s build > busola.log & - - pushd backend - npm start > backend.log & - popd - - echo "waiting for server to be up..." - while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' "$CYPRESS_DOMAIN")" != "200" ]]; do sleep 5; done - sleep 10 - + k3d kubeconfig get k3dCluster > tests/integration/fixtures/kubeconfig.yaml + export CYPRESS_DOMAIN=http://localhost:3001 cd tests/integration npm ci && ACC_AMP_TOKEN=$ACC_AMP_TOKEN npm run "test:accesibility" - name: Uploads artifacts diff --git a/README.md b/README.md index 7e1a6e5259..46dcb5ce8b 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,13 @@ For the information on how to run tests and configure them, go to the [`tests`]( ## Deploy Busola in the Kubernetes Cluster -To install Busola in the Kubernetes cluster, run: +To install Busola from release in the Kubernetes cluster set `VERSION` shell environment variable with desired release and run: + +```shell +kubectl apply -f https://github.com/kyma-project/busola/releases/download/${VERSION}/busola.yaml +``` + +To install Busola from main branch in the Kubernetes cluster, run: ```shell (cd resources && kustomize build base/ | kubectl apply -f- ) diff --git a/docs/extensibility/100-jsonata.md b/docs/extensibility/100-jsonata.md new file mode 100644 index 0000000000..2e1e52bf74 --- /dev/null +++ b/docs/extensibility/100-jsonata.md @@ -0,0 +1,63 @@ +# Use JSONata Expressions with Resource-Based Extensions + +## Scoping + +The primary data source of [JSONata](https://docs.jsonata.org/overview.html) expressions changes depending on where it's used. Starting with the root, it contains the whole resource, but whenever it's in a child whose parent has a **source** (in lists and details) or **path** (in forms) parameter, the scope changes to data from that source or path. + +Additionally, the scope in arrays changes to the array item. + +For example, for this resource: + +```yaml +spec: + name: foo + description: bar + items: + - name: item-name + details: + status: ok +``` + +The following definition has their scope changed as follows: + +```yaml +- source: spec.name # top level, scope is the same as a resource + +- source: spec # top level, scope is the same as a resource + children: + - source: name # parent has source=spec, therefore this refers to spec.name + +- children: + - source: spec.name # As there's no parent source here, the scope is still the resource + +- source: spec.items + children: + - source: name # parent data is an array, therefore scope changes to its item - this refers to spec.items[0].name + - source: details.status # refers to spec.items[0].details.status (same as above) + - source: details # this changes scope for its children again + children: + source: status # this refers to spec.items[0].details.status +``` + +## Common Variables + +Common variables are the primary means to bypass the default scoping. + +- **\$root** - always contains the reference to the resource, so any JSONata in the example above can always be `$root.spec.name`. +- **\$item** - refers to the most recent array item. When not in an array, it's equal to **\$root**. +- **\$items** - contains an array of references to all parent array items (with the last item being equal to **\$item**). +- **\$value** - when used in a JSONata other than **source** (for example **visibility**, but also other widget-specific formulas), contains the value returned by the source. +- **\$index** - exists in array components, refers to the index of the current item of an array. + +### Example + +```yaml +- widget: Table + source: spec.rules + visibility: $exists($value) + collapsibleTitle: "'Rule #' & $string($index + 1)" +``` + +## Data Sources + +Whenever data sources are provided, they are available as corresponding variable names. For more information, see [Configure the dataSources Section](90-datasources.md). diff --git a/docs/extensibility/101-preset-functions.md b/docs/extensibility/101-preset-functions.md new file mode 100644 index 0000000000..104761153a --- /dev/null +++ b/docs/extensibility/101-preset-functions.md @@ -0,0 +1,87 @@ +# JSONata Preset Functions for Resource-Based Extensions + +## canI (resourceGroupAndVersion, resourceKind) + +You can use the **canI** function to determine if a user has access rights to list a specified resource. The function comes with the following parameters: + +- **resourceGroupAndVersion**: Determines the first part of a resource URL following the pattern: `${resource group}/${resource version}`. +- **resourceKind**: Describes a resource kind. + +### Example + +```yaml +- path: spec.gateway + name: gateway + visibility: $not($canI('networking.istio.io/v1beta1', 'Gateway')) +``` + +## compareStrings (first, second) + +You can use this function to sort two strings alphabetically. The function comes with the following parameters: + +- **first**: Determines the first string to compare. +- **second**: Determines the second string to compare. + +### Example + +Here is an example from the [ResourceList widget](./50-list-and-details-widgets.md#resourcelist): + +```yaml +- widget: ResourceList + source: '$myDeployments()' + name: Example ResourceList Deployments + sort: + - source: '$item.spec.strategy.type' + compareFunction: '$compareStrings($second, $first)' + default: true +``` + +## matchByLabelSelector (item, selectorPath) + +You can use this function to match Pods using a resource selector. The function comes with the following parameters: + +- **item**: Describes a Pod to be used. +- **selectorPath**: Defines a path to selector labels from `$root`. + +### Example + +Example from [dataSources](90-datasources.md). + +```yaml +- podSelector: + resource: + kind: Pod + version: v1 + filter: '$matchByLabelSelector($item, $root.spec.selector)' +``` + +## matchEvents (item, kind, name) + +You can use this function to match Events using a resource selector. The function comes with the following parameters: + +- **item**: Describes an Event to be checked. +- **kind**: Describes the kind of the Event emitting resource. +- **name**: Describes the name of the Event emitting resource. + +### Example + +```yaml +- widget: EventList + filter: '$matchEvents($item, $root.kind, $root.metadata.name)' + name: events + defaultType: NORMAL + hideInvolvedObjects: true +``` + +## readableTimestamp (timestamp) + +You can use this function to convert time to readable time. The function comes with the following parameters: + +- **timestamp**: Defines a timestamp to convert. + +### Example + +```yaml +- source: '$readableTimestamp($item.lastTransitionTime)' + name: status.conditions.lastTransitionTime +``` diff --git a/docs/extensibility/110-presets.md b/docs/extensibility/110-presets.md new file mode 100644 index 0000000000..d0522019ef --- /dev/null +++ b/docs/extensibility/110-presets.md @@ -0,0 +1,37 @@ +# Configure the presets Section + +The **presets** section contains a list of objects that define which preset and template are used in the form view. If you specify a preset, it is displayed in the dropdown list along with the **Clear** option. When you select a preset, the form is filled with the values defined in the **value** property. + +## Available Parameters + +| Parameter | Required | Type | Description | +| ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------ | +| **name** | **Yes** | string | A name to display on the preset's dropdown. | +| **value** | **Yes** | | It contains the fields that are set when you choose the given preset from the list. | +| **default** | No | boolean | If set to `true`, it prefills the form with values defined in **value**. It defaults to `false`. | + +## Example + +```yaml +- name: template + default: true + value: + metadata: + name: my-name + spec: + description: A set description +- name: preset + value: + metadata: + name: second-one + spec: + data: regex + description: A different description + items: + - name: item-1 + value: 10 + - name: item-2 + value: 11 + - name: item-3 + value: 5 +``` diff --git a/docs/extensibility/120-resource-extensions.md b/docs/extensibility/120-resource-extensions.md new file mode 100644 index 0000000000..a2ff23c04b --- /dev/null +++ b/docs/extensibility/120-resource-extensions.md @@ -0,0 +1,72 @@ +# Configure a Config Map for Resource-Based Extensions + +You can set up your ConfigMap to handle your UI page by adding objects to the **general** section. This section contains basic information about the resource and additional options. +You can provide all the ConfigMap data sections as either JSON or YAML. + +## Extension Version + +The version is a string value that defines in which version the extension is configured. It is stored as a value of the `busola.io/extension-version` label. If the configuration is created with the **Create Extension** button, this value is provided automatically. When created manually, use the latest version number, for example, `'0.5'`. + +> [!NOTE] +> Busola supports only the two latest versions of the configuration. Whenever a new version of the configuration is proposed, go to your Extension and migrate your configuration to the latest version. + +## Available Parameters + +| Parameter | Required | Type | Description | | | | | | +| ---------------------------------- | -------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | --- | ------ | ------------------------------------------------------------------------------------------------------------------ | --- | +| **resource** | **Yes** | | A resource defined based on the following properties: | | | | | | +| **resource.kind** | **Yes** | string | A Kubernetes resource kind. | | | | | | +| **resource.version** | **Yes** | string | A Kubernetes resource version. | **resource.group** | No | string | An API group used for all the requests. It's not provided for the Kubernetes resources in the core (legacy) group. | | +| **name** | No | string | A name used a title in the navigation and on the list screen. It defaults to its resource kind. | | | | | | +| **category** | No | string | A name of the category used for the left-hand menu. By default, it's placed in the `Custom Resources` category. | | | | | | +| **icon** | No | | A suffix of an icon name used for the left-hand menu. The default value is `customized`. You can find the list of icons [here](https://sap.github.io/fundamental-react/?path=/docs/component-api-icon--primary). | | | | | | +| **scope** | No | string | It can be `namespace` or `cluster`. It defaults to `cluster`. | | | | | | +| **urlPath** | No | string | A path fragment for this resource used in the URL. Defaults to pluralized lowercase **kind**. It is used to provide an alternative URL to avoid conflicts with other resources. | | | | | | +| **defaultPlaceholder** | No | string | It is visible in an empty text placeholder. Overridden by the widget-level **placeholder**. Defaults to `-`. | | | | | | +| **description** | No | string | It displays a custom description on the resource list page. It can contain links. If the **translations** section has a translation entry with the ID that is the same as the **description** string, the translation is used. | | | | | | +| **filter** | No | string, [JSONata](100-jsonata.md) expression | An optional JSONata [filter](https://docs.jsonata.org/higher-order-functions#filter) used to filter the resources shown at the list section property. | | | | | | +| **features** | No | boolean | An object for the features configuration. | | | | | | +| **features.actions** | No | boolean | An object for the actions configuration. | | | | | | +| **features.actions.disableCreate** | No | boolean | When set to `true`, it disables the **Create** button. It defaults to `false`. | | | | | | +| **features.actions.disableEdit** | No | boolean | When set to `true`, it disables the **Edit** button. It defaults to `false`. | | | | | | +| **features.actions.disableDelete** | No | boolean | When set to `true`, it disables the **Delete** button. It defaults to `false`. | | | | | | +| **externalNodes** | No | string | A list of links to external websites. | | | | | | +| **externalNodes.category** | No | string | A name of the category. | | | | | | +| **externalNodes.scope** | No | string | It can be `namespace` or `cluster`. It defaults to `cluster`. | | | | | | +| **externalNodes.icon** | No | string | An icon that you can choose from the [Icon Explorer](https://sdk.openui5.org/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview). | | | | | | +| **externalNodes.children** | No | string | A list of child nodes containing details about the links. | | | | | | +| **externalNodes.children.label** | No | string | A displayed label. | | | | | | +| **externalNodes.children.link** | No | string, [JSONata](100-jsonata.md) expression | A link to an external website. | | | | | | + +### Example + +```yaml +resource: + kind: MyResource + version: v1alpha3 + group: networking.istio.io +name: MyResourceName +category: My Category +scope: namespace +defaultPlaceholder: '- not set -' +description: See the {{[docs](https://github.com/kyma-project/busola)}} for more information. +filter: "$filter(data, function($data) {$data.type = 'Opaque'})" +features: + actions: + disableCreate: true + disableDelete: true +externalNodes: + - category: My Category + icon: course-book + children: + - label: Example Node Label + link: 'https://github.com/kyma-project/busola' + - category: My Second Category + icon: bar-chart + scope: namespace + children: + - label: Example Node Label + link: '$string($exampleResource().link)' +``` + +For more information, see [Additional Sections for Resource-Based Extensions](130-additional-sections-resources.md). diff --git a/docs/extensibility/130-additional-sections-resources.md b/docs/extensibility/130-additional-sections-resources.md new file mode 100644 index 0000000000..a5011b41ea --- /dev/null +++ b/docs/extensibility/130-additional-sections-resources.md @@ -0,0 +1,29 @@ +# Additional Sections for Resource-Based Extensions + +## Form Section + +To customize the **form** section see the [Create forms with extensibility](./40-form-fields.md) documentation. +Views created with the extensibility [ConfigMap wizard](README.md) have a straightforward form configuration by default. + +## List Section + +The **list** section presents the resources of a kind, that is, Secrets or ConfigMaps, and comes with a few predefined columns: **Name**, **Created**, and **Labels**. +If you want to add your own columns, see [Customize UI display](./30-details-summary.md) to learn how to customize both list and details views. + +## Details Section + +The **details** section presents the resource details. To customize it, see [Customize UI display](./30-details-summary.md). The default details header contains some basic information. By default, the body is empty. + +## Value Preprocessors + +Value preprocessors are used as a middleware between a value and the actual renderer. They can transform a given value and pass it to the widget, or stop processing and render it so you can view it immediately, without passing it to the widget. + +### List of Value Preprocessors + +- **PendingWrapper** - useful when value resolves to a triple of `{loading, error, data}`: + + - For `loading` equal to `true`, it displays a loading indicator. + - For truthy `error`, it displays an error message. + - Otherwise, it passes `data` to the display component. + + Unless you need custom handling of error or loading state, we recommend using **PendingWrapper**, for example, for fields that use [data sources](90-datasources.md). diff --git a/docs/extensibility/140-static-extensions.md b/docs/extensibility/140-static-extensions.md new file mode 100644 index 0000000000..3f8e783bad --- /dev/null +++ b/docs/extensibility/140-static-extensions.md @@ -0,0 +1,36 @@ +# Configure a Config Map for Static Extensions + +You can define a static extension by adding the `busola.io/extension:statics` label to the ConfigMap. You don't need the **general** section as static extensions present data that are not connected to any resource. Instead, they may use information from the page they are displayed on using the `$embedResource` variable. You can provide all the ConfigMap data sections as either JSON or YAML. + +## Extension Version + +The version is a string value that defines in which version the extension is configured. It is stored as a value of the `busola.io/extension-version` label. When created manually, use the latest version number, for example, `'0.5'`. + +> [!NOTE] +> Busola supports only the two latest versions of the configuration. Whenever a new version of the configuration is proposed, go to your Extension and migrate your configuration to the latest version. + +## Available Parameters + +| Parameter | Required | Type | Description | +| -------------------------------- | -------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| **externalNodes** | No | string | It is used to define optional links to external websites that appear in the navigation menu. | +| **externalNodes.catagory** | No | string | A name of the category. | +| **externalNodes.scope** | No | string | It can be `namespace` or `cluster`. It defaults to `cluster`. | +| **externalNodes.icon** | No | string | An icon that you can choose from the [Icon Explorer](https://sdk.openui5.org/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview). | +| **externalNodes.children** | No | string | a list of child Nodes containing details about the links. | +| **externalNodes.children.label** | No | string | a displayed label. | +| **externalNodes.children.link** | No | string, [JSONata](100-jsonata.md) expression | a link to an external website. | + +### Example + +```yaml +general: + externalNodes: + - category: My Category + icon: course-book + children: + - label: Example Node Label + link: 'https://github.com/kyma-project/busola' +``` + +For more information on an exemplary configuration of the `External Nodes` feature in static extensions, see the [configuration example](examples/../../../examples/statics/statics-external-nodes.yaml). diff --git a/docs/extensibility/translations-section.md b/docs/extensibility/150-translations.md similarity index 97% rename from docs/extensibility/translations-section.md rename to docs/extensibility/150-translations.md index 1c155f397b..74b3670fd2 100644 --- a/docs/extensibility/translations-section.md +++ b/docs/extensibility/150-translations.md @@ -1,4 +1,4 @@ -# _translations_ section +# Configure Translations This optional section contains all available languages formatted for [i18next](https://www.i18next.com/) either as YAML or JSON, based on their paths. When a name is provided for a widget, that value can be used as the key, and the value is the translation for a specific language. diff --git a/docs/extensibility/160-wizard-extensions.md b/docs/extensibility/160-wizard-extensions.md new file mode 100644 index 0000000000..1b736d550a --- /dev/null +++ b/docs/extensibility/160-wizard-extensions.md @@ -0,0 +1,41 @@ +# Configure a Config Map for Wizard Extensions + +You can set up your ConfigMap to handle a custom wizard by adding objects to the **general** section. This section contains basic information about the created resources and additional options. +You can provide all the ConfigMap data sections as either JSON or YAML. + +## Extension Version + +The version is a string value that defines in which version the extension is configured. It is stored as a value of the `busola.io/extension-version` label. If the configuration is created with the **Create Extension** button, this value is provided automatically. When created manually, use the latest version number, for example, `'0.5'`. + +> [!NOTE] +> Busola supports only the two latest versions of the configuration. Whenever a new version of the configuration is proposed, go to your Extension and migrate your configuration to the latest version. + +## Available Parameters + +| Parameter | Required | Type | Description | +| -------------------------------- | -------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| **externalNodes** | No | string | It is used to define optional links to external websites that appear in the navigation menu. | +| **externalNodes.catagory** | No | string | A name of the category. | +| **externalNodes.scope** | No | string | It can be `namespace` or `cluster`. It defaults to `cluster`. | +| **externalNodes.icon** | No | string | An icon that you can choose from the [Icon Explorer](https://sdk.openui5.org/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview). | +| **externalNodes.children** | No | string | a list of child Nodes containing details about the links. | +| **externalNodes.children.label** | No | string | a displayed label. | +| **externalNodes.children.link** | No | string, [JSONata](100-jsonata.md) expression | a link to an external website. | + +### Example + +```yaml +id: mywizard +name: Create a MyResource +resources: + myresource: + kind: MyResource + group: busola.example.com + version: v1 + myservice: + kind: MyService + group: busola.example.com + version: v1 +``` + +See [Additional Sections for Wizard Extensions](170-additional-sections-wizard.md) for more information on the available sections. diff --git a/docs/extensibility/170-additional-sections-wizard.md b/docs/extensibility/170-additional-sections-wizard.md new file mode 100644 index 0000000000..abdd0404fd --- /dev/null +++ b/docs/extensibility/170-additional-sections-wizard.md @@ -0,0 +1,50 @@ +# Additional Sections for Wizard Extensions + +## steps Section + +Each wizard consists of steps. The steps section contains their definitions. + +### Available Parameters + +| Parameter | Required | Type | Description | | +| --------------- | -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- | --- | +| **name** | **Yes** | string | The step name displayed on the step navigation and in the step header. | | +| **resource** | **Yes** | string | The default resource identifier for this step. | | +| **form** | **Yes** | string | the form definition. This is analogous to the contents of the [form section](./40-form-fields.md) of the resource extension. | | +| **description** | No | string | Additional details about the step, shown only when the step is active. | | + +## defaults Section + +The defaults section is optional. If present, not all resources must be covered. This section contains a map of default values for specific resources. It is appended to the basic skeleton resources created based on the data provided in the [general section](160-wizard-extensions.md). + +## Example + +```yaml +data: + defaults: + qqq: + spec: + string-ref: foo + subqqq: + metadata: + labels: + example: example + steps: + - name: First step + description: this is the first step + resource: qqq + form: + - id: foo + path: spec.string-ref + name: string ref + trigger: [sr] + - path: spec.double-ref.name + name: double ref name + visibility: false + overwrite: false + subscribe: + init: spec."string-ref" + sr: spec."string-ref" +``` + +For the example of usage, check the [Get started with functions](../../examples/wizard/README.md) wizard. diff --git a/docs/extensibility/custom-extensions.md b/docs/extensibility/80-custom-extensions.md similarity index 51% rename from docs/extensibility/custom-extensions.md rename to docs/extensibility/80-custom-extensions.md index ef645b8a57..a98c3ceddc 100644 --- a/docs/extensibility/custom-extensions.md +++ b/docs/extensibility/80-custom-extensions.md @@ -1,6 +1,6 @@ # Custom Extensions -Busola's custom extension feature allows you to design fully custom user interfaces beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays not covered by the built-in components. +With Busola's custom extension feature, you can design fully custom user interfaces beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays that are not covered by the built-in components. ## Getting Started @@ -15,10 +15,10 @@ EXTENSIBILITY_CUSTOM_COMPONENTS: Creating a custom extension is as straightforward as setting up a ConfigMap with the following sections: -- `data.general`: Contains configuration details -- `data.customHtml`: Defines static HTML content -- `data.customScript`: Adds dynamic behavior to your extension. +- **data.general**: Contains configuration details +- **data.customHtml**: Defines static HTML content +- **data.customScript**: Adds dynamic behavior to your extension. -Once your ConfigMap is ready, add it to your cluster, and Busola will load and display your custom UI. +Once your ConfigMap is ready, add it to your cluster. Then, Busola loads and displays your custom UI. -See this [example](./../../examples/custom-extension/README.md), to learn more. +For more information, see this [example](./../../examples/custom-extension/README.md). diff --git a/docs/extensibility/90-datasources.md b/docs/extensibility/90-datasources.md new file mode 100644 index 0000000000..2c7490570d --- /dev/null +++ b/docs/extensibility/90-datasources.md @@ -0,0 +1,59 @@ +# Configure the dataSources Section + +The optional **dataSources** section contains an object that maps a data source name to a data source configuration object. The data source name, preceded by a dollar sign `$`, is used in the **source** expression. + +Data sources are provided in all [JSONata](100-jsonata.md) formulas as functions to call. For example, `{ "source": $myRelatedResource().metadata.labels }` returns the `metadata.labels` of the related resource. + +When you provide the whole request, you can access individual resources using the `items` field, for example `{ "widget": "Table", "source": "$myRelatedResources().items" }`. + +## Available Parameters + +| Parameter | Required | Type | Description | +| -------------------------- | -------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **resource** | **Yes** | | A resource defined based on the following properties: | +| **resource.kind** | **Yes** | string | A Kubernetes resource kind. | +| **resource.version** | **Yes** | string | A Kubernetes resource version. | +| **resource.name** | No | string | A resource name. If left empty, all resources of a given type are matched. | +| **resource.group** | No | string | A Kubernetes resource group. It's not provided for the Kubernetes resources in the core (legacy) group. | +| **resource.namespace** | No | string | The name of the resource's namespace. It defaults to the original resource's namespace. If set to `null`, cluster-wide resources or resources in all namespaces are matched. | +| **ownerLabelSelectorPath** | No | | The path to original object's selector type property. For example, `spec.selector.matchLabels` for Deployment, used to select matching Pods. | +| **filter** | No | [JSONata](100-jsonata.md) expression | It allows you to write a custom matching logic. It uses the `item` variable to point to the current item of the related kind, and the `root` variable to point to the original resource. It returns a boolean value. You can also filter using the [`matchByLabelSelector` function](101-preset-functions.md#matchbylabelselectoritem-selectorpath) to see the matched Pods. To do that, provide the Pods as `$item`, and path to the labels. | + +## Examples + +```yaml +details: + body: + - widget: ResourceList + source: '$myPods()' +dataSources: + myPods: + resource: + kind: Pod + version: v1 + ownerLabelSelectorPath: spec.selector.matchLabels +``` + +```yaml +details: + body: + - widget: ResourceList + path: '$mySecrets' +dataSources: + mySecrets: + resource: + kind: Secret + version: v1 + namespace: + filter: + '$root.spec.secretName = $item.metadata.name and $root.metadata.namespace + = $item.metadata.namespace' +``` + +```yaml +podSelector: + resource: + kind: Pod + version: v1 + filter: '$matchByLabelSelector($item, $root.spec.selector)' +``` diff --git a/docs/extensibility/datasources-section.md b/docs/extensibility/datasources-section.md deleted file mode 100644 index 495e82991e..0000000000 --- a/docs/extensibility/datasources-section.md +++ /dev/null @@ -1,65 +0,0 @@ -# _dataSources_ section - -The optional **dataSources** section contains an object that maps a data source name to a data source configuration object. The data source name, preceded by a dollar sign '\$', is used in the **source** expression. - -Data sources are provided in all [JSONata](jsonata.md) formulas as functions to call. For example, `{ "source": $myRelatedResource().metadata.labels }` returns the `metadata.labels` of the related resource. - -When you provide the whole request, you can access individual resources using the `items` field, for example `{ "widget": "Table", "source": "$myRelatedResources().items" }`. - -## Data source configuration object fields - -Busola uses the following fields to build the related resource URL and filter the received data. - -- **resource**: - - **kind** - _[required]_ Kubernetes resource kind. - - **group** - Kubernetes resource group. Not provided for Kubernetes resources in the core (also called legacy) group. - - **version** - _[required]_ Kubernetes resource version. - - **namespace** - the resource's Namespace name; it defaults to the original resource's Namespace. If set to `null`, cluster-wide resources or resources in all Namespaces are matched. - - **name** - a specific resource name; leave empty to match all resources of a given type. -- **ownerLabelSelectorPath** - the path to original object's **selector** type property; for example, `spec.selector.matchLabels` for Deployment, used to select matching Pods. -- **filter** - a [JSONata](jsonata.md) function enabling the user to write a custom matching logic. It uses the following variables: - - - **item** - the current item of the related kind. - - **root** - the original resource. - - This function should return a boolean value. - You can also use the [`matchByLabelSelector` function](jsonata.md#matchbylabelselectoritem-selectorpath) to see the matched Pods. To do that, provide the Pods as `$item`, and path to the labels. - -## Examples - -```yaml -details: - body: - - widget: ResourceList - source: '$myPods()' -dataSources: - myPods: - resource: - kind: Pod - version: v1 - ownerLabelSelectorPath: spec.selector.matchLabels -``` - -```yaml -details: - body: - - widget: ResourceList - path: '$mySecrets' -dataSources: - mySecrets: - resource: - kind: Secret - version: v1 - namespace: - filter: - '$root.spec.secretName = $item.metadata.name and $root.metadata.namespace - = $item.metadata.namespace' -``` - -```yaml -podSelector: - resource: - kind: Pod - version: v1 - filter: '$matchByLabelSelector($item, $root.spec.selector)' -``` diff --git a/docs/extensibility/jsonata.md b/docs/extensibility/jsonata.md deleted file mode 100644 index 2e08c8bd90..0000000000 --- a/docs/extensibility/jsonata.md +++ /dev/null @@ -1,175 +0,0 @@ -# Jsonata preset functions for resource-based extensions - -**Table of Contents** - -- [Overview](#overview) -- [Scoping](#scoping) -- [Preset functions](#preset-functions) - - [_canI_](#caniresourcegroupandversion-resourcekind) - - [_compareStrings_](#comparestringsfirst-second) - - [_matchByLabelSelector_](#matchbylabelselectoritem-selectorpath) - - [_matchEvents_](#matcheventsitem-kind-name) - -## Overview - -This document describes how to use JSONata expressions [JSONata](https://docs.jsonata.org/overview.html) throughout the extensions. - -## Scoping - -The primary data source of JSONata expressions changes depending on where it's used. Starting with the root, it contains the whole resource, but whenever it's in a child whose parent has a **source** (in lists and details) or **path** (in forms) parameter, the scope changes to data from that source or path. - -Additionally, scope in arrays changes to the array item. - -For example, for this resource: - -```yaml -spec: - name: foo - description: bar - items: - - name: item-name - details: - status: ok -``` - -The following definition has their scope changed as follows: - -```yaml -- source: spec.name # top level, scope is the same as resource - -- source: spec # top level, scope is the same as resource - children: - - source: name # parent has source=spec, therefore this refers to spec.name - -- children: - - source: spec.name # however there's no parent source here, therefore scope is still the resource - -- source: spec.items - children: - - source: name # parent data is an array, therefore scope changes to it's item - this refers to spec.items[0].name - - source: details.status # refers to spec.items[0].details.status (same as above) - - source: details # this changes scope for it's children again - children: - source: status # so this refers to spec.items[0].details.status -``` - -## Common variables - -Common variables are the primary means to bypass the default scoping. - -- **\$root** - always contains the reference to the resource, so any JSONata in the example above can always be `$root.spec.name`. -- **\$item** - refers to the most recent array item. When not in an array, it's equal to **\$root**. -- **\$items** - contains an array of references to all parent array items (with the last item being equal to **\$item**). -- **\$value** - when used in a JSONata other than **source** (for example **visibility**, but also other widget-specific formulas), contains the value returned by the source. -- **\$index** - exists in array components, refers to the index of the current item of an array. - -#### Example - -```yaml -- widget: Table - source: spec.rules - visibility: $exists($value) - collapsibleTitle: "'Rule #' & $string($index + 1)" -``` - -## Data sources - -Whenever data sources are provided, they are available as corresponding variable names. See [data sources](datasources-section.md) section for more details. - -## Preset functions - -### canI(resourceGroupAndVersion, resourceKind) - -You can use this function to determine if a user has access rights for listing a specified resource. - -#### Function parameters - -- **resourceGroupAndVersion** - the first part of a resource URL following the pattern: `${resource group}/${resource version}`. -- **resourceKind** - resource kind. - -#### Example - -```yaml -- path: spec.gateway - name: gateway - visibility: $not($canI('networking.istio.io/v1beta1', 'Gateway')) -``` - -### compareStrings(first, second) - -You can use this function to sort two strings alphabetically. - -#### Function parameters - -- **first** - first string to compare. -- **second** - second string to compare. - -#### Example - -Here is an example from the [ResourceList widget](./50-list-and-details-widgets.md#resourcelist): - -```yaml -- widget: ResourceList - source: '$myDeployments()' - name: Example ResourceList Deployments - sort: - - source: '$item.spec.strategy.type' - compareFunction: '$compareStrings($second, $first)' - default: true -``` - -### matchByLabelSelector(item, selectorPath) - -You can use this function to match Pods using a resource selector. - -#### Function parameters - -- **item** - Pod to be used. -- **selectorPath** - path to selector labels from `$root`. - -#### Example - -Example from [dataSources](datasources-section.md). - -```yaml -- podSelector: - resource: - kind: Pod - version: v1 - filter: '$matchByLabelSelector($item, $root.spec.selector)' -``` - -### matchEvents(item, kind, name) - -You can use this function to match Events using a resource selector. - -#### Function parameters - -- **item** - Event to be checked. -- **kind** - kind of the Event emitting resource. -- **name** - name of the Event emitting resource. - -#### Example - -```yaml -- widget: EventList - filter: '$matchEvents($item, $root.kind, $root.metadata.name)' - name: events - defaultType: NORMAL - hideInvolvedObjects: true -``` - -### readableTimestamp(timestamp) - -You can use this function to convert time to readable time. - -#### Function parameters - -- **timestamp** - timestamp to convert. - -#### Example - -```yaml -- source: '$readableTimestamp($item.lastTransitionTime)' - name: status.conditions.lastTransitionTime -``` diff --git a/docs/extensibility/presets-section.md b/docs/extensibility/presets-section.md deleted file mode 100644 index 76b9e952a2..0000000000 --- a/docs/extensibility/presets-section.md +++ /dev/null @@ -1,37 +0,0 @@ -# _presets_ section - -The **presets** section contains a list of objects that define which preset and template are used in the form view. If you specify a preset, it is displayed in the dropdown list along with the **Clear** option. When you select a preset, the form is filled with the values defined in the **value** property. - -## preset configuration object fields - -- **name** - _[required]_ a name to display on the preset's dropdown, -- **value** - _[required]_ contains fields that are set when you choose this preset from the list. -- **default** - For `default` equal to `true`, it prefills form with values defined in the **value** property. Defaults to `false`. - -## Example - -```yaml -- name: template - default: true - value: - metadata: - name: my-name - spec: - description: A set description -- name: preset - value: - metadata: - name: second-one - spec: - data: regex - description: A different description - items: - - name: item-1 - value: 10 - - name: item-2 - value: 11 - - name: item-3 - value: 5 -``` - -Preset list with one entry defined as default diff --git a/docs/extensibility/resources.md b/docs/extensibility/resources.md deleted file mode 100644 index 3439a0b2fa..0000000000 --- a/docs/extensibility/resources.md +++ /dev/null @@ -1,113 +0,0 @@ -# Config Map for resource-based extensions - -**Table of Contents** - -- [Overview](#overview) -- [Extension version](#extension-version) -- [_general_ section](#general-section) -- [_form_ section](#form-section) -- [_list_ section](#list-section) -- [_details_ section](#details-section) -- [Value preprocessors](#value-preprocessors) - - [List of value preprocessors](#list-of-value-preprocessors) - -## Overview - -This document describes the required ConfigMap setup that you need to configure in order to handle your CRD UI page. -You can provide all the ConfigMap data sections as either JSON or YAML. - -## Extension version - -The version is a string value that defines in which version the extension is configured. It is stored as a value of the `busola.io/extension-version` label. If the configuration is created with the **Create Extension** button, this value is provided automatically. When created manually, use the latest version number, for example, `'0.5'`. - -> **NOTE:**: Busola supports only the two latest versions of the configuration. Whenever a new version of the configuration is proposed, go to your Extension and migrate your configuration to the latest version. - -## _general_ section - -The **general** section is required and contains basic information about the resource and additional options. - -### Item parameters - -- **resource** - _[required]_ - information about the resource. - - **kind** - _[required]_ Kubernetes kind of the resource. - - **version** - _[required]_ API version used for all requests. - - **group** - API group used for all requests. Not provided for Kubernetes resources in the core (also called legacy) group. -- **name** - title used in the navigation and on the list screen. It defaults to its resource kind. -- **category** - the name of a category used for the left-hand menu. By default, it's placed in the `Custom Resources` category. -- **icon** - suffix of an icon name used for the left-hand menu. The default value is `customized`. You can find the list of icons [here](https://sap.github.io/fundamental-react/?path=/docs/component-api-icon--primary). -- **scope** - either `namespace` or `cluster`. Defaults to `cluster`. -- **urlPath** - path fragment for this resource used in the URL. Defaults to pluralized lowercase **kind**. Used to provide an alternative URL to avoid conflicts with other resources. -- **defaultPlaceholder** - to be shown in place of an empty text placeholder. Overridden by the widget-level **placeholder**. Defaults to `-`. -- **description** - displays a custom description on the resource list page. It can contain links. If the **translations** section has a translation entry with the ID that is the same as the **description** string, the translation is used. -- **filter** - optional [JSONata](jsonata.md) [filter](https://docs.jsonata.org/higher-order-functions#filter) used to filter the resources shown at the list section property. -- **features** - an optional object for the features configuration. - - **actions** - an optional object for the actions configuration. - - **disableCreate** - when set to `true`, it disables the **Create** button. Defaults to `false`. - - **disableEdit** - when set to `true`, it disables the **Edit** button. Defaults to `false`. - - **disableDelete** - when set to `true`, it disables the **Delete** button. Defaults to `false`. -- **externalNodes** - an optional list of links to external websites. - - **category** - a category name - - **scope** - either `namespace` or `cluster`. Defaults to `cluster`. - - **icon** - an optional icon. Go to [Icon Explorer](https://sdk.openui5.org/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview) to find a list of the available icons. - - **children** - a list of child nodes containing details about the links - - **label** - a displayed label - - **link** - a link to an external website. You can provide a [JSONata](jsonata.md) function. - -### Example - -```yaml -resource: - kind: MyResource - version: v1alpha3 - group: networking.istio.io -name: MyResourceName -category: My Category -scope: namespace -defaultPlaceholder: '- not set -' -description: See the {{[docs](https://github.com/kyma-project/busola)}} for more information. -filter: "$filter(data, function($data) {$data.type = 'Opaque'})" -features: - actions: - disableCreate: true - disableDelete: true -externalNodes: - - category: My Category - icon: course-book - children: - - label: Example Node Label - link: 'https://github.com/kyma-project/busola' - - category: My Second Category - icon: bar-chart - scope: namespace - children: - - label: Example Node Label - link: '$string($exampleResource().link)' -``` - -## _form_ section - -To customize the **form** section see the [Create forms with extensibility](./40-form-fields.md) documentation. -Views created with the extensibility [ConfigMap wizard](README.md) have a straightforward form configuration by default. - -## _list_ section - -The **list** section presents the resources of a kind, that is, Secrets or ConfigMaps, and comes with a few predefined columns: **Name**, **Created**, and **Labels**. -If you want to add your own columns, see [Customize UI display](./30-details-summary.md) to learn how to customize both list and details views. - -## _details_ section - -The **details** section presents the resource details. To customize it, see [Customize UI display](./30-details-summary.md). The default details header contains some basic information. By default, the body is empty. - -## Value preprocessors - -Value preprocessors are used as a middleware between a value and the actual renderer. They can transform a given value and pass it to the widget; or stop processing and render it so you can view it immediately, without passing it to the widget. - -### List of value preprocessors - -- **PendingWrapper** - useful when value resolves to a triple of `{loading, error, data}`: - - - For `loading` equal to `true`, it displays a loading indicator. - - For truthy `error`, it displays an error message. - - Otherwise, it passes `data` to the display component. - - Unless you need custom handling of error or loading state, we recommend using **PendingWrapper**, for example, for fields that use [data sources](./datasources-section.md). diff --git a/docs/extensibility/statics.md b/docs/extensibility/statics.md deleted file mode 100644 index 2acd49847b..0000000000 --- a/docs/extensibility/statics.md +++ /dev/null @@ -1,58 +0,0 @@ -# Config Map for static extensions - -**Table of Contents** - -- [Overview](#overview) -- [Static extension label](#static-extension-label) -- [Extension version](#extension-version) -- [_general_ section](#general-section) -- [_injections_ section](#injections-section) - -## Overview - -This document describes the required ConfigMap setup that you need to configure to handle a static extension. -You can provide all the ConfigMap data sections as either JSON or YAML. - -## Static extension label - -To define a static extension, add the `busola.io/extension:statics` label to the ConfigMap. - -## Extension version - -The version is a string value that defines in which version the extension is configured. It is stored as a value of the `busola.io/extension-version` label. When created manually, use the latest version number, for example, `'0.5'`. - -> **NOTE:** Busola supports only the two latest versions of the configuration. Whenever a new version of the configuration is proposed, go to your Extension and migrate your configuration to the latest version. - -## _general_ section - -The **general** section is not required as static extensions present data that are not connected to any resource. Instead, they may use information from the page they are displayed on via variable `$embedResource`. - -### _externalNodes_ - -The **externalNodes** parameter allows you to define optional links to external websites that appear in the navigation menu. - -- **externalNodes** - an optional list of links to external websites. - - **category** - a category name. - - **scope** - either `namespace` or `cluster`. Defaults to `cluster`. - - **icon** - an optional icon. Go to [Icon Explorer](https://sdk.openui5.org/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview) to find the list of the available icons. - - **children** - a list of child Nodes containing details about the links. - - **label** - a displayed label - - **link** - a link to an external website. You can provide a [JSONata](jsonata.md) function. - -### Example - -```yaml -general: - externalNodes: - - category: My Category - icon: course-book - children: - - label: Example Node Label - link: 'https://github.com/kyma-project/busola' -``` - -To see an exemplary configuration of the `External Nodes` feature in static extensions, check the [configuration example](examples/../../../examples/statics/statics-external-nodes.yaml). - -## _injections_ section - -For more information, read the [widget injections overview](./70-widget-injection.md). diff --git a/docs/extensibility/wizard.md b/docs/extensibility/wizard.md deleted file mode 100644 index 6975d949cd..0000000000 --- a/docs/extensibility/wizard.md +++ /dev/null @@ -1,76 +0,0 @@ -# Config Map for wizard extensions - -**Table of Contents** - -- [Overview](#overview) -- [Extension version](#extension-version) -- [_general_ section](#general-section) -- [_steps_ section](#steps-section) -- [_defaults_ section](#defaults-section) - -## Overview - -This document describes the required ConfigMap setup that you need to configure in order to handle a custom wizard. -You can provide all the ConfigMap data sections as either JSON or YAML. - -## Extension version - -The version is a string value that defines in which version the extension is configured. It is stored as a value of the `busola.io/extension-version` label. If the configuration is created with the **Create Extension** button, this value is provided automatically. When created manually, use the latest version number, for example, `'0.5'`. - -> **NOTE:**: Busola supports only the two latest versions of the configuration. Whenever a new version of the configuration is proposed, go to your Extension and migrate your configuration to the latest version. - -## _general_ section - -The **general** section is required and contains basic information about the created resources and additional options. - -### Item parameters - -- **id** - _[required]_ - an identifier used to reference the wizard to trigger its opening. -- **resources** - _[required]_ - information about the resources created by the wizard. This is a key value map with values consisting of: - - **kind** - _[required]_ Kubernetes kind of the resource. - - **version** - _[required]_ API version used for all requests. - - **group** - API group used for all requests. Not provided for Kubernetes resources in the core (also called legacy) group. -- **name** - wizard window title. - -### Example - -```yaml -id: mywizard -name: Create a MyResource -resources: - myresource: - kind: MyResource - group: busola.example.com - version: v1 - myservice: - kind: MyService - group: busola.example.com - version: v1 -``` - -## _steps_ section - -Each wizard consists of steps. This section contains their definitions. - -### Item parameters - -Each step contains the following parameters: - -- **name** - _[required]_ - the name of the step displayed on the step navigation and in the step header -- **description** - extra details about the step, shown only when the step is active -- **resource** - _[required]_ - the identifier of the default resource for this step -- **form** - _[required]_ - the definition of the form - this is analogous to the contents of the [_form_ section](./40-form-fields.md) of the resource extension - -## _defaults_ section - -This section is optional; if present, not all resources must be covered. This section contains a map of default values for specific resources. It is appended to the basic skeleton resources created based on the data provided in the [_general_ section](#general-section). - -### Example - -```yaml -myresource: - spec: - enabled: true -``` - -For the example of usage, check the [Get started with functions](../../examples/wizard/README.md) wizard. diff --git a/src/components/Clusters/views/ClusterOverview/ClusterStats.js b/src/components/Clusters/views/ClusterOverview/ClusterStats.js index e235f2214b..2a3655a1a6 100644 --- a/src/components/Clusters/views/ClusterOverview/ClusterStats.js +++ b/src/components/Clusters/views/ClusterOverview/ClusterStats.js @@ -7,6 +7,7 @@ import { Card, CardHeader, Title } from '@ui5/webcomponents-react'; import { CountingCard } from 'shared/components/CountingCard/CountingCard'; import { bytesToHumanReadable, + cpusToHumanReadable, getBytes, } from 'resources/Namespaces/ResourcesUsage'; import { @@ -128,9 +129,11 @@ export default function ClusterStats({ nodesData }) { color="var(--sapChart_OrderedColor_5)" value={roundTwoDecimals(cpu.usage)} max={roundTwoDecimals(cpu.capacity)} - additionalInfo={`${roundTwoDecimals( - cpu.usage, - )}m / ${roundTwoDecimals(cpu.capacity)}m`} + additionalInfo={`${cpusToHumanReadable(cpu.usage, { + unit: 'm', + })} / ${cpusToHumanReadable(cpu.capacity, { + unit: 'm', + })}`} /> diff --git a/src/components/Nodes/NodeResources/NodeResources.js b/src/components/Nodes/NodeResources/NodeResources.js index fa2807735b..d0426d9625 100644 --- a/src/components/Nodes/NodeResources/NodeResources.js +++ b/src/components/Nodes/NodeResources/NodeResources.js @@ -3,6 +3,7 @@ import { UI5RadialChart } from 'shared/components/UI5RadialChart/UI5RadialChart' import { Card, CardHeader } from '@ui5/webcomponents-react'; import { roundTwoDecimals } from 'shared/utils/helpers'; import './NodeResources.scss'; +import { cpusToHumanReadable } from '../../../resources/Namespaces/ResourcesUsage.js'; export function NodeResources({ metrics, resources }) { const { t } = useTranslation(); @@ -19,10 +20,10 @@ export function NodeResources({ metrics, resources }) { - Math.round(num * Math.pow(10, places)) / Math.pow(10, places); + Math.round((num + Number.EPSILON) * Math.pow(10, places)) / + Math.pow(10, places); const getPercentageFromUsage = (value, total) => { if (total === 0) { @@ -15,16 +16,16 @@ const getPercentageFromUsage = (value, total) => { return Math.round((100 * value) / total); }; -const formatCpu = cpuStr => Math.ceil(parseInt(cpuStr || '0') / 1000_000); -const formatMemory = memoryStr => +const formatKiToGiMemory = memoryStr => round(parseInt(memoryStr || '0') / 1024 / 1024, 1); const createUsageMetrics = (node, metricsForNode) => { - const cpuUsage = formatCpu(metricsForNode?.usage.cpu); - const memoryUsage = formatMemory(metricsForNode?.usage.memory); - const cpuCapacity = parseInt(node.status.allocatable?.cpu || '0'); - const memoryCapacity = formatMemory(node.status.allocatable?.memory); + const cpuUsage = getCpus(metricsForNode?.usage.cpu); + const memoryUsage = formatKiToGiMemory(metricsForNode?.usage.memory); + const cpuCapacity = getCpus(node.status.allocatable?.cpu || '0'); + const memoryCapacity = formatKiToGiMemory(node.status.allocatable?.memory); + console.log(cpuCapacity); const cpuPercentage = getPercentageFromUsage(cpuUsage, cpuCapacity); const memoryPercentage = getPercentageFromUsage(memoryUsage, memoryCapacity); @@ -176,11 +177,11 @@ export function calcNodeResources(pods) { return { limits: { - cpu: nodeResources.limits.cpu * 1000, + cpu: nodeResources.limits.cpu, memory: nodeResources.limits.memory / Math.pow(1024, 3), }, requests: { - cpu: nodeResources.requests.cpu * 1000, + cpu: nodeResources.requests.cpu, memory: nodeResources.requests.memory / Math.pow(1024, 3), }, }; diff --git a/src/components/Nodes/nodeQueries.test.js b/src/components/Nodes/nodeQueries.test.js index 9e073be4cc..935cad7850 100644 --- a/src/components/Nodes/nodeQueries.test.js +++ b/src/components/Nodes/nodeQueries.test.js @@ -24,11 +24,11 @@ describe('Calculate resources for node', () => { }, expectedValue: { limits: { - cpu: 10, + cpu: 0.01, memory: 100.0 / 1024, }, requests: { - cpu: 20, + cpu: 0.02, memory: 200.0 / 1024, }, }, @@ -49,11 +49,11 @@ describe('Calculate resources for node', () => { }, expectedValue: { limits: { - cpu: 22, + cpu: 0.022, memory: 220.0 / 1024, }, requests: { - cpu: 45, + cpu: 0.045, memory: 450.0 / 1024, }, }, @@ -68,11 +68,11 @@ describe('Calculate resources for node', () => { }, expectedValue: { limits: { - cpu: 7, + cpu: 0.007, memory: 70.0 / 1024, }, requests: { - cpu: 14, + cpu: 0.014, memory: 140.0 / 1024, }, }, diff --git a/src/resources/Namespaces/ResourcesUsage.js b/src/resources/Namespaces/ResourcesUsage.js index 188c32b9fe..0dbe1cd3d1 100644 --- a/src/resources/Namespaces/ResourcesUsage.js +++ b/src/resources/Namespaces/ResourcesUsage.js @@ -3,7 +3,7 @@ import { useGetList } from 'shared/hooks/BackendAPI/useGet'; import { Spinner } from 'shared/components/Spinner/Spinner'; import { useTranslation } from 'react-i18next'; -import { getSIPrefix } from 'shared/helpers/siPrefixes'; +import { formatResourceUnit } from 'shared/helpers/resources.js'; import { Card, CardHeader } from '@ui5/webcomponents-react'; const MEMORY_SUFFIX_POWER = { @@ -21,6 +21,7 @@ const MEMORY_SUFFIX_POWER = { const CPU_SUFFIX_POWER = { m: 1e-3, + n: 1e-9, }; export function getBytes(memoryStr) { @@ -50,12 +51,12 @@ export function getCpus(cpuString) { export function bytesToHumanReadable(bytes) { if (!bytes) return bytes; - return getSIPrefix(bytes, true, { withoutSpace: true }).string; + return formatResourceUnit(bytes, true, { withoutSpace: true }); } -export function cpusToHumanReadable(cpus) { +export function cpusToHumanReadable(cpus, { fixed = 0, unit = '' } = {}) { if (!cpus) return cpus; - return cpus / MEMORY_SUFFIX_POWER['m'] + 'm'; + return formatResourceUnit(cpus, false, { withoutSpace: true, fixed, unit }); } const MemoryRequestsCircle = ({ resourceQuotas, isLoading }) => { diff --git a/src/shared/helpers/resources.js b/src/shared/helpers/resources.js new file mode 100644 index 0000000000..37583352bc --- /dev/null +++ b/src/shared/helpers/resources.js @@ -0,0 +1,56 @@ +const SI_PREFIXES = { + p: 1e-12, + n: 1e-9, + µ: 1e-6, + m: 1e-3, + '': 1, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, +}; + +const SI_PREFIXES_BINARY = { + Ki: 2 ** 10, + Mi: 2 ** 20, + Gi: 2 ** 30, + Ti: 2 ** 40, + Pi: 2 ** 50, +}; + +/* +More precise round method. +Want 1.005 to be rounded to 1.01 we need to add Number.EPSILON to fix the float inaccuracy + */ +const preciseRound = (num, places) => + Math.round((num + Number.EPSILON) * Math.pow(10, places)) / + Math.pow(10, places); + +export function formatResourceUnit( + amount, + binary = false, + { unit = '', withoutSpace = true, fixed = 2 } = {}, +) { + const prefixMap = binary ? SI_PREFIXES_BINARY : SI_PREFIXES; + const infix = withoutSpace ? '' : ' '; + + if (unit && prefixMap[unit]) { + const value = (amount / prefixMap[unit]).toFixed(fixed); + return `${value}${infix}${unit}`; + } + + const coreValue = preciseRound(amount, 2).toFixed(fixed); + + let output = `${coreValue}${infix}${unit}`; + Object.entries(prefixMap).forEach(([prefix, power]) => { + const tmpValue = amount / power; + if (tmpValue >= 1) { + const value = preciseRound(tmpValue, 2).toFixed(fixed); + output = `${value}${infix}${prefix}${unit}`; + } + }); + + return output.string; +} diff --git a/src/shared/helpers/siPrefixes.js b/src/shared/helpers/siPrefixes.js deleted file mode 100644 index a2693926b4..0000000000 --- a/src/shared/helpers/siPrefixes.js +++ /dev/null @@ -1,59 +0,0 @@ -const SI_PREFIXES = { - p: 1e-12, - n: 1e-9, - µ: 1e-6, - m: 1e-3, - '': 1, - k: 1e3, - M: 1e6, - G: 1e9, - T: 1e12, - P: 1e15, - E: 1e18, -}; - -const SI_PREFIXES_BINARY = { - Ki: 2 ** 10, - Mi: 2 ** 20, - Gi: 2 ** 30, - Ti: 2 ** 40, - Pi: 2 ** 50, -}; - -export function getSIPrefix( - amount, - binary = false, - { unit = '', withoutSpace = true, fixed = 2 } = {}, -) { - const prefixMap = binary ? SI_PREFIXES_BINARY : SI_PREFIXES; - const infix = withoutSpace ? '' : ' '; - - const coreValue = ( - Math.round((+amount + Number.EPSILON) * 100) / 100 - ).toFixed(fixed); - let output = { - raw: amount, - value: coreValue, - rounded: coreValue, - prefix: '', - string: `${coreValue}${infix}${unit}`, - }; - Object.entries(prefixMap).forEach(([prefix, power]) => { - const tmpValue = amount / power; - if (tmpValue >= 1) { - const value = ( - Math.round((tmpValue + Number.EPSILON) * 100) / 100 - ).toFixed(fixed); - output = { - raw: tmpValue, - value, - rounded: value * power, - prefix, - unit: `${prefix}${unit}`, - string: `${value}${infix}${prefix}${unit}`, - }; - } - }); - - return output; -}