From 6d6acd5fb90cd330e469c5c99dd4ee5ab01c7412 Mon Sep 17 00:00:00 2001 From: Christian Karidas <105549337+chriskari@users.noreply.github.com> Date: Wed, 19 Jun 2024 13:40:35 +0200 Subject: [PATCH 1/9] fix: responsiveness of labels (#2980) --- src/shared/components/Labels/Labels.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shared/components/Labels/Labels.scss b/src/shared/components/Labels/Labels.scss index 2706e10eae..a22528603c 100644 --- a/src/shared/components/Labels/Labels.scss +++ b/src/shared/components/Labels/Labels.scss @@ -1,6 +1,11 @@ .labels { display: grid; justify-items: start; + grid-template-columns: minmax(100px, max-content); + + > * { + max-width: 100%; + } } .token { From 3f5b352c51c7fc93cf857c4826d090474f987467 Mon Sep 17 00:00:00 2001 From: Oliwia Gowor <72342415+OliwiaGowor@users.noreply.github.com> Date: Wed, 19 Jun 2024 23:46:05 +0200 Subject: [PATCH 2/9] feat: Redesign of sort dialog (#2979) * adjust SortModalPanel * add reset button & adjust test * adjust test * cleanup * review fix * add selected prop --- .../components/GenericList/GenericList.js | 4 + .../components/GenericList/SortModalPanel.js | 126 ++++++++++++------ .../GenericList/SortModalPanel.scss | 4 + src/shared/components/Modal/Modal.js | 12 +- .../extensibility/ext-test-pizzas.spec.js | 4 +- 5 files changed, 107 insertions(+), 43 deletions(-) create mode 100644 src/shared/components/GenericList/SortModalPanel.scss diff --git a/src/shared/components/GenericList/GenericList.js b/src/shared/components/GenericList/GenericList.js index 6b121b81c9..f0c5a30513 100644 --- a/src/shared/components/GenericList/GenericList.js +++ b/src/shared/components/GenericList/GenericList.js @@ -171,6 +171,10 @@ export const GenericList = ({ sort={sort} setSort={setSort} disabled={!entries.length} + defaultSort={{ + name: sortBy && Object.keys(sortBy)[0], + order: 'ASC', + }} /> )} diff --git a/src/shared/components/GenericList/SortModalPanel.js b/src/shared/components/GenericList/SortModalPanel.js index f5c3cfe488..ea5ca6ca80 100644 --- a/src/shared/components/GenericList/SortModalPanel.js +++ b/src/shared/components/GenericList/SortModalPanel.js @@ -1,10 +1,24 @@ import { useState } from 'react'; -import { Button, FlexBox, RadioButton } from '@ui5/webcomponents-react'; +import { + Button, + CustomListItem, + GroupHeaderListItem, + List, + RadioButton, + Text, +} from '@ui5/webcomponents-react'; import { Modal } from '../Modal/Modal'; import { Tooltip } from '../Tooltip/Tooltip'; import { useTranslation } from 'react-i18next'; +import './SortModalPanel.scss'; -export const SortModalPanel = ({ sortBy, sort, setSort, disabled = false }) => { +export const SortModalPanel = ({ + sortBy, + sort, + setSort, + disabled = false, + defaultSort, +}) => { const [order, setOrder] = useState(sort.order); const [name, setName] = useState(sort.name); @@ -21,9 +35,20 @@ export const SortModalPanel = ({ sortBy, sort, setSort, disabled = false }) => { ); + const handleReset = () => { + setOrder(defaultSort.order); + setName(defaultSort.name); + }; + return ( + {t('common.buttons.reset')} + + } actions={onClose => [ + } + /> + + )} + ); } diff --git a/src/components/KymaModules/KymaModulesAddModule.js b/src/components/KymaModules/KymaModulesAddModule.js index ea69ae0a28..39a52b5842 100644 --- a/src/components/KymaModules/KymaModulesAddModule.js +++ b/src/components/KymaModules/KymaModulesAddModule.js @@ -25,7 +25,9 @@ export default function KymaModulesAddModule(props) { '/apis/operator.kyma-project.io/v1beta2/namespaces/kyma-system/kymas', ); - const resourceName = kymaResources?.items[0].metadata.name; + const resourceName = + kymaResources?.items.find(kymaResource => kymaResource?.status)?.metadata + .name || kymaResources?.items[0]?.metadata?.name; const kymaResourceUrl = `/apis/operator.kyma-project.io/v1beta2/namespaces/kyma-system/kymas/${resourceName}`; const { data: modules } = useGet(modulesResourceUrl, { diff --git a/src/components/KymaModules/KymaModulesQuery.js b/src/components/KymaModules/KymaModulesQuery.js new file mode 100644 index 0000000000..91337f6d27 --- /dev/null +++ b/src/components/KymaModules/KymaModulesQuery.js @@ -0,0 +1,30 @@ +import { useGet } from 'shared/hooks/BackendAPI/useGet'; + +export function useKymaModulesQuery(skip = false) { + const { + data: kymaResources, + loading: loadingKymaResources, + error: errorKymaResources, + } = useGet( + '/apis/operator.kyma-project.io/v1beta2/namespaces/kyma-system/kymas', + ); + + const resourceName = + kymaResources?.items.find(kymaResource => kymaResource?.status)?.metadata + .name || kymaResources?.items[0]?.metadata?.name; + const kymaResourceUrl = `/apis/operator.kyma-project.io/v1beta2/namespaces/kyma-system/kymas/${resourceName}`; + + const { data: kymaResource, loading: loadingKyma, error: errorKyma } = useGet( + kymaResourceUrl, + { + pollingInterval: 3000, + skip: !resourceName || errorKymaResources, + }, + ); + + return { + modules: kymaResource?.status?.modules || [], + error: errorKymaResources || errorKyma, + loading: loadingKymaResources || loadingKyma, + }; +} diff --git a/src/shared/components/CountingCard/CountingCard.tsx b/src/shared/components/CountingCard/CountingCard.tsx index cbe3ca0136..07033e41a3 100644 --- a/src/shared/components/CountingCard/CountingCard.tsx +++ b/src/shared/components/CountingCard/CountingCard.tsx @@ -18,6 +18,7 @@ type CountingCardProps = { resourceUrl: string; isClusterResource: boolean; className: string; + additionalContent?: React.ReactNode; }; export const CountingCard = ({ @@ -28,6 +29,7 @@ export const CountingCard = ({ resourceUrl, isClusterResource = false, className = '', + additionalContent, }: CountingCardProps) => { const { t } = useTranslation(); const { namespaceUrl, clusterUrl } = useUrl(); @@ -80,6 +82,7 @@ export const CountingCard = ({ {t('common.buttons.learn-more')} )} + {additionalContent && additionalContent} ); From 3a41c20b3365c3b08b3246d18443a89df7b09122 Mon Sep 17 00:00:00 2001 From: Mateusz Wisniewski Date: Thu, 20 Jun 2024 12:56:05 +0200 Subject: [PATCH 4/9] feat: gha cypress namespace tests (#2981) * feat: gha cypress workflow * rename of the file * adjust metrics and versions * adjust version of k3d * bump timeout for terminating status * trigger * change exist to visible * revert exist * fix: retries * fix: retries * paths --- .github/workflows/busola-local-build.yml | 2 ++ ...yml => pull-integration-namespace-k3d.yml} | 30 +++++++++++-------- .../tests/namespace/z-run-after.spec.js | 21 +++++++++---- 3 files changed, 34 insertions(+), 19 deletions(-) rename .github/workflows/{cypress-tests.yml => pull-integration-namespace-k3d.yml} (75%) diff --git a/.github/workflows/busola-local-build.yml b/.github/workflows/busola-local-build.yml index f268c1f209..510ebc2034 100644 --- a/.github/workflows/busola-local-build.yml +++ b/.github/workflows/busola-local-build.yml @@ -5,6 +5,7 @@ on: branches: - main paths: + - ".github/workflows/busola-local-build.yml" - "backend/**" - "public/**" - "src/**" @@ -15,6 +16,7 @@ on: pull_request_target: types: [opened, edited, synchronize, reopened, ready_for_review] paths: + - ".github/workflows/busola-local-build.yml" - "backend/**" - "public/**" - "src/**" diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/pull-integration-namespace-k3d.yml similarity index 75% rename from .github/workflows/cypress-tests.yml rename to .github/workflows/pull-integration-namespace-k3d.yml index 1b1c5218f2..76c86efa5e 100644 --- a/.github/workflows/cypress-tests.yml +++ b/.github/workflows/pull-integration-namespace-k3d.yml @@ -1,21 +1,25 @@ -# fragments shamelessly stolen from https://github.com/kyma-project/api-gateway/tree/main/.github -name: Cypress PoC -on: workflow_dispatch +name: pull-integration-namespace-k3d + +on: + pull_request: + types: [opened, edited, synchronize, reopened, ready_for_review] + paths: + - ".github/workflows/pull-integration-namespace-k3d.yml" + - "resources/**" + - "tests/**" + - "nginx/**" + - "src/**" + - "tests/**" + jobs: - test: + run-test: runs-on: ubuntu-latest - # environment: - # name: release steps: - - uses: gardenlinux/workflow-telemetry-action@6f19ac2411a52a120abb74c812592b44f165d05c # pin@v1 + - uses: gardenlinux/workflow-telemetry-action@v2 with: - metric_frequency: 1 - proc_trace_min_duration: 10 - proc_trace_chart_max_count: 50 comment_on_pr: false - uses: actions/checkout@v4 with: - # ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Create Single Cluster uses: AbsaOSS/k3d-action@4e8b3239042be1dc0aed6c5eb80c13b18200fc79 #v2.4.0 @@ -54,14 +58,14 @@ jobs: cd tests/integration npm ci && npm run "test:namespace" - name: Uploads artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: cypress-${{ github.job }} path: tests/integration/cypress/ retention-days: 90 - name: Upload Busola logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: busola-logs-${{ github.job }} diff --git a/tests/integration/tests/namespace/z-run-after.spec.js b/tests/integration/tests/namespace/z-run-after.spec.js index ba5c95b1a5..2cbfaa1c21 100644 --- a/tests/integration/tests/namespace/z-run-after.spec.js +++ b/tests/integration/tests/namespace/z-run-after.spec.js @@ -17,15 +17,24 @@ context('Clean up Namespace', () => { checkIfResourceIsRemoved: false, selectSearchResult: true, }); - }); - it('Check if the Namespace is terminated (step 2)', { retries: 3 }, () => { cy.get('ui5-table-row') .find('.status-badge') .contains('Terminating'); - - cy.get('ui5-table') - .contains(Cypress.env('NAMESPACE_NAME')) - .should('not.exist', { timeout: 50000 }); }); + + it( + 'Check if the Namespace is terminated (step 2)', + { + retries: { + runMode: 3, + openMode: 3, + }, + }, + () => { + cy.get('ui5-table') + .contains(Cypress.env('NAMESPACE_NAME')) + .should('not.exist', { timeout: 50000 }); + }, + ); }); From d3868e96c3050d815d876ee7ea7a8ec9a727e7e9 Mon Sep 17 00:00:00 2001 From: Damian Badura <45110612+dbadura@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:04:05 +0200 Subject: [PATCH 5/9] fix: Display correct value for resource quota when spec is not in `requests` (#2983) * take data also from root * fix memory --- src/resources/Namespaces/ResourcesUsage.js | 8 ++++---- src/resources/ResourceQuotas/ResourceQuotaList.js | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/resources/Namespaces/ResourcesUsage.js b/src/resources/Namespaces/ResourcesUsage.js index 46d4e2a7e5..a3eba5bdd6 100644 --- a/src/resources/Namespaces/ResourcesUsage.js +++ b/src/resources/Namespaces/ResourcesUsage.js @@ -58,7 +58,7 @@ const MemoryRequestsCircle = ({ resourceQuotas, isLoading }) => { (sum, quota) => sum + getBytes( - quota.status?.hard?.['requests.memory'] || quota.status?.hard?.cpu, + quota.status?.hard?.['requests.memory'] || quota.status?.hard?.memory, ), 0, ); @@ -66,7 +66,7 @@ const MemoryRequestsCircle = ({ resourceQuotas, isLoading }) => { (sum, quota) => sum + getBytes( - quota.status?.used?.['requests.memory'] || quota.status?.used?.cpu, + quota.status?.used?.['requests.memory'] || quota.status?.used?.memory, ), 0, ); @@ -94,11 +94,11 @@ const MemoryLimitsCircle = ({ resourceQuotas, isLoading }) => { } const totalLimits = resourceQuotas.reduce( - (sum, quota) => sum + getBytes(quota.status?.hard?.['limits.memory']), //should we sum it or take the max number? + (sum, quota) => sum + getBytes(quota.status?.hard?.['limits.memory']), 0, ); const totalUsage = resourceQuotas.reduce( - (sum, quota) => sum + getBytes(quota.status?.used?.['limits.memory']), //should we sum it or take the max number? + (sum, quota) => sum + getBytes(quota.status?.used?.['limits.memory']), 0, ); diff --git a/src/resources/ResourceQuotas/ResourceQuotaList.js b/src/resources/ResourceQuotas/ResourceQuotaList.js index 81592094e5..0b598cb653 100644 --- a/src/resources/ResourceQuotas/ResourceQuotaList.js +++ b/src/resources/ResourceQuotas/ResourceQuotaList.js @@ -11,29 +11,28 @@ export function ResourceQuotaList(props) { { header: t('resource-quotas.headers.limits.cpu'), value: quota => - (quota.spec?.hard && quota.spec?.hard['limits.cpu']) || - EMPTY_TEXT_PLACEHOLDER, + quota.spec?.hard?.['limits.cpu'] || EMPTY_TEXT_PLACEHOLDER, }, { header: t('resource-quotas.headers.limits.memory'), value: quota => - (quota.spec?.hard && quota.spec?.hard['limits.memory']) || - EMPTY_TEXT_PLACEHOLDER, + quota.spec?.hard?.['limits.memory'] || EMPTY_TEXT_PLACEHOLDER, }, { header: t('resource-quotas.headers.requests.cpu'), value: quota => - (quota.spec?.hard && quota.spec?.hard['requests.cpu']) || + quota.spec?.hard?.['requests.cpu'] || + quota.spec?.hard?.cpu || EMPTY_TEXT_PLACEHOLDER, }, { header: t('resource-quotas.headers.requests.memory'), value: quota => - (quota.spec?.hard && quota.spec?.hard['requests.memory']) || + quota.spec?.hard?.['requests.memory'] || + quota.spec?.hard?.memory || EMPTY_TEXT_PLACEHOLDER, }, ]; - return ( ); } + export default ResourceQuotaList; From 8bd409bd041523554862a98e4846ab09904cf47e Mon Sep 17 00:00:00 2001 From: Damian Badura <45110612+dbadura@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:44:06 +0200 Subject: [PATCH 6/9] improve error handling (#2982) --- .../Extensibility/components/ResourceList.js | 1 + src/components/Extensibility/components/Widget.js | 7 +++++-- .../components/ResourcesList/ResourcesList.js | 4 ++-- src/shared/hooks/BackendAPI/config.js | 14 +++++++++----- src/shared/utils/helpers.js | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/components/Extensibility/components/ResourceList.js b/src/components/Extensibility/components/ResourceList.js index 94bf52435c..65b22deed6 100644 --- a/src/components/Extensibility/components/ResourceList.js +++ b/src/components/Extensibility/components/ResourceList.js @@ -25,6 +25,7 @@ const getProperNamespacePart = (givenNamespace, currentNamespace) => { return `/namespaces/${currentNamespace}`; } }; + export function ResourceList({ value, structure, diff --git a/src/components/Extensibility/components/Widget.js b/src/components/Extensibility/components/Widget.js index 28fd4326ef..44511f9200 100644 --- a/src/components/Extensibility/components/Widget.js +++ b/src/components/Extensibility/components/Widget.js @@ -164,8 +164,11 @@ export function Widget({ const sanitizedValue = stringifyIfBoolean(childValue); - return Array.isArray(childValue) && !Renderer.array ? ( - childValue.map(valueItem => ( + if (sanitizedValue?.loading) { + return null; + } + return Array.isArray(sanitizedValue) && !Renderer.array ? ( + sanitizedValue.map(valueItem => ( , document.body, )} - {!(error && error.toString().includes('is forbidden')) && ( + {!(error && error.status === 'Definition not found') && ( getConfigFn('backendAddress'); export class HttpError extends Error { - constructor(message, statusCode, code) { - if ([401, 403].includes(statusCode)) { + constructor(message, status, code) { + if ([401, 403].includes(status)) { super('You are not allowed to perform this operation'); } else { super(message); } this.code = code; - this.statusCode = statusCode; + this.status = status; this.originalMessage = message; } } @@ -31,7 +30,12 @@ export async function throwHttpError(response) { const errorMessage = message || `${status} ${statusText || getReasonPhrase(status)}`; - return new Error(errorMessage); + // When response status is 404 and boyd is empty it means that resoruce definition is not registered in k8s + let textStatus = ''; + if (response.status === 404) { + textStatus = 'Definition not found'; + } + return new HttpError(errorMessage, textStatus, response.status); } } } diff --git a/src/shared/utils/helpers.js b/src/shared/utils/helpers.js index 20cf0ac129..fb2cbd68b0 100644 --- a/src/shared/utils/helpers.js +++ b/src/shared/utils/helpers.js @@ -108,7 +108,7 @@ export const prettifyKind = kind => { export const getErrorMessage = (error, message = null) => { let errorNotification = message ? message - : 'An error occured. The component cannot be rendered.'; + : 'The component cannot be rendered'; if (error?.message && typeof error?.originalMessage !== 'object') { errorNotification += `: ${error.message} `; From aded8f12ddfc262e55bdc4bef4b2bfc8da39abd3 Mon Sep 17 00:00:00 2001 From: Christian Karidas <105549337+chriskari@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:58:05 +0200 Subject: [PATCH 7/9] feat: new pagination (#2973) * new pagination * responsive styling * adjusted unit test * further adjustments of unit test * feat: added hintbutton to pagination --- public/i18n/en.yaml | 6 + src/components/Preferences/OtherSettings.tsx | 13 +- .../components/GenericList/GenericList.js | 8 +- .../GenericList/Pagination/Pagination.js | 198 +++++++++++++----- .../GenericList/Pagination/Pagination.scss | 109 ++++++++-- .../Pagination/test/Pagination.test.js | 33 ++- src/state/preferences/pageSizeAtom.ts | 2 + 7 files changed, 290 insertions(+), 79 deletions(-) diff --git a/public/i18n/en.yaml b/public/i18n/en.yaml index 3532742d76..d72f8d6249 100644 --- a/public/i18n/en.yaml +++ b/public/i18n/en.yaml @@ -1482,8 +1482,14 @@ settings: title: Interface language: Language other: + info: Changes to the page size only take effect locally. To set the global default page size, go to Preferences. default-page-size: Default Page Size title: Other + all: All + results-per-page: Results per page + page: Page + of: of {{pagesCount}} + total-items: '{{itemsCount}} items' theme: Theme stateful-sets: description: <0>StatefulSet is used for the management of Pods' deployment and scaling. diff --git a/src/components/Preferences/OtherSettings.tsx b/src/components/Preferences/OtherSettings.tsx index b34b55c61d..3935f542d5 100644 --- a/src/components/Preferences/OtherSettings.tsx +++ b/src/components/Preferences/OtherSettings.tsx @@ -1,8 +1,10 @@ import { useTranslation } from 'react-i18next'; import { useRecoilState } from 'recoil'; import { Select, Option } from '@ui5/webcomponents-react'; -import { pageSizeState } from 'state/preferences/pageSizeAtom'; -const AVAILABLE_PAGE_SIZES = [10, 20, 50]; +import { + AVAILABLE_PAGE_SIZES, + pageSizeState, +} from 'state/preferences/pageSizeAtom'; export default function OtherSettings() { const { t } = useTranslation(); @@ -29,6 +31,13 @@ export default function OtherSettings() { {available_size} ))} + ); diff --git a/src/shared/components/GenericList/GenericList.js b/src/shared/components/GenericList/GenericList.js index f0c5a30513..b8f0809bf5 100644 --- a/src/shared/components/GenericList/GenericList.js +++ b/src/shared/components/GenericList/GenericList.js @@ -97,7 +97,12 @@ export const GenericList = ({ } }; - const pageSize = useRecoilValue(pageSizeState); + const globalPageSize = useRecoilValue(pageSizeState); + const [pageSize, setLocalPageSize] = useState(globalPageSize); + useEffect(() => { + setLocalPageSize(globalPageSize); + }, [globalPageSize]); + pagination = useMemo(() => { if (pagination) return { itemsPerPage: pageSize, ...(pagination || {}) }; return undefined; @@ -408,6 +413,7 @@ export const GenericList = ({ currentPage={currentPage} itemsPerPage={pagination.itemsPerPage} onChangePage={setCurrentPage} + setLocalPageSize={setLocalPageSize} /> )} diff --git a/src/shared/components/GenericList/Pagination/Pagination.js b/src/shared/components/GenericList/Pagination/Pagination.js index d9cf9c12e5..2d0e385a53 100644 --- a/src/shared/components/GenericList/Pagination/Pagination.js +++ b/src/shared/components/GenericList/Pagination/Pagination.js @@ -1,25 +1,34 @@ -import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; import PropTypes from 'prop-types'; -import { Button, Text, Icon } from '@ui5/webcomponents-react'; +import { Button, Text, Icon, Input, FlexBox } from '@ui5/webcomponents-react'; import classNames from 'classnames'; - +import { Select, Option } from '@ui5/webcomponents-react'; +import { AVAILABLE_PAGE_SIZES } from 'state/preferences/pageSizeAtom'; +import { HintButton } from 'shared/components/DescriptionHint/DescriptionHint'; import './Pagination.scss'; const makePartitions = (currentPage, pagesCount) => { const radius = 2; - return new Array(pagesCount) - .fill(0) - .map((_, i) => i) - .filter( - i => - i < radius || - i > pagesCount - radius - 1 || - Math.abs(i - currentPage + 1) <= radius / 2, - ); + const partitions = []; + // add the current page and the pages in its radius + for (let i = 1; i <= pagesCount; i++) { + if (i === 1 || Math.abs(currentPage - i) <= radius || i === pagesCount) { + partitions.push(i); + } + } + // if between the radius and first/last elements are further pages insert placeholders + if (partitions[1] !== 2) { + partitions.splice(1, 0, '...'); + } + if (partitions[partitions.length - 2] !== pagesCount - 1) { + partitions.splice(partitions.length - 1, 0, '...'); + } + return partitions; }; const Link = ({ children, isInteractable, isCurrent, onClick, ...props }) => { - const className = classNames('nav-link', { + const className = classNames('page-link', { current: isCurrent, interactable: isInteractable, }); @@ -41,50 +50,144 @@ export const Pagination = ({ itemsPerPage, currentPage, onChangePage, + setLocalPageSize, }) => { + const { t } = useTranslation(); + const [showInfo, setShowInfo] = useState(false); + const pagesCount = Math.ceil(itemsTotal / itemsPerPage); const partitions = makePartitions(currentPage, pagesCount); + const onSelectionChange = event => { + const selectedSize = event.detail.selectedOption.value; + setLocalPageSize(parseInt(selectedSize)); + }; + return (
- {itemsTotal} items - onChangePage(currentPage - 1)} - aria-label="Previous page" - > - + + {t('settings.other.results-per-page') + ':'} + + + - +
- {partitions.map((current, i) => ( - - {i > 0 && current - partitions[i - 1] > 1 && ...} - onChangePage(current + 1)} - > - {current + 1} - - - ))} +
+ onChangePage(1)} + aria-label="First page" + > + + + onChangePage(currentPage - 1)} + aria-label="Previous page" + > + + + {partitions.map((current, i) => + current === currentPage ? ( + + + {t('settings.other.page')} + + { + const newValue = Number(event.target.typedInValue); + if (newValue >= 1 && newValue <= pagesCount) + onChangePage(newValue); + }} + value={currentPage} + type="Number" + /> + + {t('settings.other.of', { pagesCount })} + + + ) : ( + onChangePage(current)} + > + {current} + + ), + )} + onChangePage(currentPage + 1)} + aria-label="Next page" + > + + + onChangePage(pagesCount)} + aria-label="Last page" + > + + +
- onChangePage(currentPage + 1)} - aria-label="Next page" - > - - +
+ + {t('settings.other.total-items', { itemsCount: itemsTotal })} + +
); }; @@ -94,4 +197,5 @@ Pagination.propTypes = { itemsPerPage: PropTypes.number, currentPage: PropTypes.number.isRequired, onChangePage: PropTypes.func.isRequired, + setLocalPageSize: PropTypes.func.isRequired, }; diff --git a/src/shared/components/GenericList/Pagination/Pagination.scss b/src/shared/components/GenericList/Pagination/Pagination.scss index bb8b3b2449..ef616923a6 100644 --- a/src/shared/components/GenericList/Pagination/Pagination.scss +++ b/src/shared/components/GenericList/Pagination/Pagination.scss @@ -1,30 +1,101 @@ .pagination { display: flex; - justify-content: 'center'; - align-items: baseline; -} + justify-content: space-between; + align-items: center; + padding: 0 1rem; -.pagination > * { - margin: auto; -} + .pagesize-selector-container { + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + + .pagesize-selector { + max-width: 80px; + } + } + + .page-links-container { + display: flex; + flex-direction: row; + align-items: center; + position: absolute; + left: 50%; + transform: translate(-50%); + + .page-link { + border: none; + background: none; + color: var(--sapNeutralColor); + font-size: var(--sapFontSize); + + i::before { + color: var(--sapBrandColor); + font-size: 80%; + } + + &.current { + font-weight: bolder; + } -.nav-link { - border: none; - background: none; - color: var(--sapNeutralColor); - font-size: var(--sapFontSize); + &.interactable { + color: var(--sapBrandColor); + cursor: pointer; + } + } - i::before { - color: var(--sapBrandColor); - font-size: 80%; + .page-input-text, + #first-page-link, + #last-page-link { + display: none; + } + + .page-input { + max-width: 40px; + text-align: center; + } } +} + +@container (max-width: 750px) { + .pagination { + .pagesize-selector-container { + #descriptionOpener-pagination, + .pagesize-label { + display: none; + } + } + + .page-links-container { + position: relative; + left: auto; + transform: none; - &.current { - font-weight: bolder; + .page-link { + display: none; + } + + .page-input-text, + #first-page-link, + #last-page-link, + #previous-page-link, + #next-page-link { + display: inherit; + } + } + + .items-number { + display: none; + } } +} + +@container (max-width: 350px) { + .pagination { + justify-content: center; - &.interactable { - color: var(--sapBrandColor); - cursor: pointer; + .pagesize-selector-container { + display: none; + } } } diff --git a/src/shared/components/GenericList/Pagination/test/Pagination.test.js b/src/shared/components/GenericList/Pagination/test/Pagination.test.js index 54f6179bc7..f0bea8336d 100644 --- a/src/shared/components/GenericList/Pagination/test/Pagination.test.js +++ b/src/shared/components/GenericList/Pagination/test/Pagination.test.js @@ -1,4 +1,3 @@ -import React from 'react'; import { render, fireEvent, waitFor, act } from '@testing-library/react'; import { Pagination } from 'shared/components/GenericList/Pagination/Pagination'; import { ThemeProvider } from '@ui5/webcomponents-react'; @@ -6,38 +5,48 @@ import '@ui5/webcomponents-icons/dist/AllIcons.js'; describe('Pagination', () => { it('Renders valid count of pages', () => { - const { queryByText } = render( + const { container, queryByText } = render( , ); - expect(queryByText('1')).toBeInTheDocument(); + expect(container.querySelector('ui5-input').value).toBe('1'); expect(queryByText('2')).toBeInTheDocument(); expect(queryByText('3')).not.toBeInTheDocument(); }); it('Renders valid count of pages - custom page size', () => { - const { queryByText } = render( + const { container, queryByText, queryAllByText } = render( , ); expect(queryByText('1')).toBeInTheDocument(); - expect(queryByText('2')).toBeInTheDocument(); + expect(queryByText('2')).not.toBeInTheDocument(); expect(queryByText('3')).toBeInTheDocument(); - expect(queryByText('4')).not.toBeInTheDocument(); + expect(queryByText('4')).toBeInTheDocument(); + expect(container.querySelector('ui5-input').value).toBe('5'); + expect(queryByText('6')).toBeInTheDocument(); + expect(queryByText('7')).toBeInTheDocument(); + expect(queryByText('8')).not.toBeInTheDocument(); + expect(queryByText('9')).toBeInTheDocument(); + + const placeholders = queryAllByText('...'); + expect(placeholders.length).toBe(2); }); it('Fire events', async () => { @@ -49,6 +58,7 @@ describe('Pagination', () => { currentPage={5} itemsPerPage={20} onChangePage={callback} + setLocalPageSize={jest.fn()} /> , ); @@ -68,18 +78,19 @@ describe('Pagination', () => { }); it('Disables correct links', () => { - const { getByText, getByLabelText, rerender } = render( + const { container, getByText, getByLabelText, rerender } = render( , ); expect(getByLabelText('Previous page')).toBeDisabled(); - expect(getByText('1')).toBeDisabled(); + expect(container.querySelector('ui5-input')).not.toBeDisabled(); expect(getByText('2')).not.toBeDisabled(); expect(getByText('3')).not.toBeDisabled(); expect(getByLabelText('Next page')).not.toBeDisabled(); @@ -91,6 +102,7 @@ describe('Pagination', () => { itemsPerPage={20} currentPage={3} onChangePage={jest.fn()} + setLocalPageSize={jest.fn()} /> , ); @@ -104,6 +116,7 @@ describe('Pagination', () => { itemsPerPage={20} currentPage={2} onChangePage={jest.fn()} + setLocalPageSize={jest.fn()} /> , ); diff --git a/src/state/preferences/pageSizeAtom.ts b/src/state/preferences/pageSizeAtom.ts index c136128012..05403cb131 100644 --- a/src/state/preferences/pageSizeAtom.ts +++ b/src/state/preferences/pageSizeAtom.ts @@ -6,6 +6,8 @@ type PageSize = number; const PAGE_SIZE_STORAGE_KEY = 'busola.page-size'; const DEFAULT_PAGE_SIZE = 20; +export const AVAILABLE_PAGE_SIZES = [10, 20, 50]; + export const pageSizeState: RecoilState = atom({ key: 'pageSizeState', default: DEFAULT_PAGE_SIZE, From ae8fbdaddf5b45e99e20f20768465206e045e69b Mon Sep 17 00:00:00 2001 From: Damian Badura <45110612+dbadura@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:52:24 +0200 Subject: [PATCH 8/9] fix filtering pods (#2993) --- .../NamespaceWorkloads/NamespaceWorkloadsHelpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloadsHelpers.js b/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloadsHelpers.js index f9b4e8b2e3..22d3cc4b92 100644 --- a/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloadsHelpers.js +++ b/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloadsHelpers.js @@ -4,9 +4,9 @@ export function getHealthyReplicasCount(resource) { } export function getHealthyStatusesCount(pods) { - const successStatuses = ['Running', 'Succeeded']; + const successStatuses = ['running', 'succeeded']; return pods?.filter(p => - successStatuses.includes(p.status.phase.toUpperCase()), + successStatuses.includes(p.status.phase.toLowerCase()), )?.length; } From 2c7bb2cb9c0b289883fc89306b3b2cc6fcd2c7e9 Mon Sep 17 00:00:00 2001 From: Damian Badura <45110612+dbadura@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:20:05 +0200 Subject: [PATCH 9/9] feat: Add statistical card to extensibility (#2962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * initial implementaion * add Statistical card to extensibility * use placeholder * add early version of documentation * add resource url * add ability to confgure statistical card resource url * improve code * fix fmt * Format docs * Add example for status * Display single Statistical Card on Cluster Overview, rename target * Add title Monitoring and Health title in details, display health widgets for extensibility * Add documentation * Adjust namespace details * Remove unused import * Show learn more automatically * Format docs * Apply suggestions from code review Co-authored-by: Małgorzata Świeca * Format docs and modify example --------- Co-authored-by: Agata Kucharska Co-authored-by: Agata Kucharska Co-authored-by: Małgorzata Świeca --- docs/extensibility/30-details-summary.md | 95 +++++++++++------- .../50-list-and-details-widgets.md | 74 ++++++++++++-- docs/extensibility/70-widget-injection.md | 1 + .../display-widgets/StatisticalCard.png | Bin 0 -> 16851 bytes public/i18n/en.yaml | 2 +- .../views/ClusterOverview/ClusterStats.js | 9 +- .../Extensibility/ExtensibilityDetails.js | 20 ++++ .../Extensibility/ExtensibilityInjections.js | 3 + .../Extensibility/components/ConditionList.js | 2 +- .../components/StatisticalCard.js | 60 +++++++++++ .../Extensibility/components/index.js | 2 + .../Extensibility/hooks/useJsonata.ts | 8 +- src/resources/Namespaces/NamespaceDetails.js | 9 +- .../NamespaceWorkloads/NamespaceWorkloads.js | 76 +++++++------- src/resources/Namespaces/ResourcesUsage.js | 50 +++++---- .../ReplicaSets/ReplicaSetDetails.js | 45 +++++---- .../components/CountingCard/CountingCard.tsx | 24 +++-- .../ResourceDetails/ResourceDetails.js | 13 +-- .../ResourceHealthCard/ResourceHealthCard.js | 24 +++++ 19 files changed, 368 insertions(+), 149 deletions(-) create mode 100644 docs/extensibility/assets/display-widgets/StatisticalCard.png create mode 100644 src/components/Extensibility/components/StatisticalCard.js create mode 100644 src/shared/components/ResourceHealthCard/ResourceHealthCard.js diff --git a/docs/extensibility/30-details-summary.md b/docs/extensibility/30-details-summary.md index 006856e4cd..cfc4920f12 100644 --- a/docs/extensibility/30-details-summary.md +++ b/docs/extensibility/30-details-summary.md @@ -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 | | --------------------- | -------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -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 diff --git a/docs/extensibility/50-list-and-details-widgets.md b/docs/extensibility/50-list-and-details-widgets.md index e3848ac808..18aa855c77 100644 --- a/docs/extensibility/50-list-and-details-widgets.md +++ b/docs/extensibility/50-list-and-details-widgets.md @@ -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) @@ -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) @@ -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 ``` Example of a condition list widget @@ -578,6 +581,63 @@ See the following example: Example of a ResourceRefs widget +### `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 +``` + +Example of a StatisticalCard widget + ### `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. diff --git a/docs/extensibility/70-widget-injection.md b/docs/extensibility/70-widget-injection.md index 04d850c1b1..2ba9120657 100644 --- a/docs/extensibility/70-widget-injection.md +++ b/docs/extensibility/70-widget-injection.md @@ -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 diff --git a/docs/extensibility/assets/display-widgets/StatisticalCard.png b/docs/extensibility/assets/display-widgets/StatisticalCard.png new file mode 100644 index 0000000000000000000000000000000000000000..f1e4ba9393da922d77fb17f86a04db51c649b4e8 GIT binary patch literal 16851 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 J$m + import('../../../Extensibility/ExtensibilityInjections'), +); + export default function ClusterStats({ nodesData }) { const { t } = useTranslation(); @@ -99,7 +103,7 @@ export default function ClusterStats({ nodesData }) { ...spacing.sapUiMediumMarginTopBottom, }} > - {t('cluster-overview.statistics.title')} + {t('common.headers.monitoring-and-health')}
@@ -282,6 +286,7 @@ export default function ClusterStats({ nodesData }) { />
)} +
); diff --git a/src/components/Extensibility/ExtensibilityDetails.js b/src/components/Extensibility/ExtensibilityDetails.js index 8984a5c060..c3912908c8 100644 --- a/src/components/Extensibility/ExtensibilityDetails.js +++ b/src/components/Extensibility/ExtensibilityDetails.js @@ -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 ( ( + + ), + ] + : [] + } description={description} createResourceForm={ExtensibilityCreate} resourceSchema={resMetaData} diff --git a/src/components/Extensibility/ExtensibilityInjections.js b/src/components/Extensibility/ExtensibilityInjections.js index 610b0da0d8..db2a7d8125 100644 --- a/src/components/Extensibility/ExtensibilityInjections.js +++ b/src/components/Extensibility/ExtensibilityInjections.js @@ -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; @@ -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} /> ); }; diff --git a/src/components/Extensibility/components/ConditionList.js b/src/components/Extensibility/components/ConditionList.js index 8270ddc588..810e297397 100644 --- a/src/components/Extensibility/components/ConditionList.js +++ b/src/components/Extensibility/components/ConditionList.js @@ -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; } diff --git a/src/components/Extensibility/components/StatisticalCard.js b/src/components/Extensibility/components/StatisticalCard.js new file mode 100644 index 0000000000..ce41147e2d --- /dev/null +++ b/src/components/Extensibility/components/StatisticalCard.js @@ -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 ( +
+ +
+ ); +} + +StatisticalCard.array = true; diff --git a/src/components/Extensibility/components/index.js b/src/components/Extensibility/components/index.js index 97528e682f..bc1d923758 100644 --- a/src/components/Extensibility/components/index.js +++ b/src/components/Extensibility/components/index.js @@ -23,6 +23,7 @@ import { FeaturedCard } from './FeaturedCard/FeaturedCard'; import { APIRuleHost } from './APIRules/APIRuleHost'; import { PendingWrapper } from './PendingWrapper'; +import { StatisticalCard } from './StatisticalCard'; export const widgets = { Null: () => '', @@ -44,6 +45,7 @@ export const widgets = { ResourceLink, ResourceList, ResourceRefs, + StatisticalCard, Table, Tabs, Text, diff --git a/src/components/Extensibility/hooks/useJsonata.ts b/src/components/Extensibility/hooks/useJsonata.ts index 93ac60e32d..f42ff7282d 100644 --- a/src/components/Extensibility/hooks/useJsonata.ts +++ b/src/components/Extensibility/hooks/useJsonata.ts @@ -1,12 +1,12 @@ -import { useState, useEffect, useContext } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { last, mapValues } from 'lodash'; import { jsonataWrapper } from '../helpers/jsonataWrapper'; import { - Resource, - DataSourcesContextType, DataSourcesContext, + DataSourcesContextType, + Resource, } from '../contexts/DataSources'; type JsonataValue = [string, Error | null]; @@ -15,7 +15,7 @@ type JsonataFunction = { ( query: string, extras: { [key: string]: any }, - defaultValue: any, + defaultValue?: any, ): JsonataValue; async: ( query: string, diff --git a/src/resources/Namespaces/NamespaceDetails.js b/src/resources/Namespaces/NamespaceDetails.js index 7a0234f625..ed1644386b 100644 --- a/src/resources/Namespaces/NamespaceDetails.js +++ b/src/resources/Namespaces/NamespaceDetails.js @@ -15,7 +15,6 @@ import NamespaceCreate from './NamespaceCreate'; import { AllNamespacesDetails } from './AllNamespacesDetails'; import { useSetRecoilState } from 'recoil'; -import { spacing } from '@ui5/webcomponents-react-base'; import { ResourceDescription } from 'resources/Namespaces'; export function NamespaceDetails(props) { @@ -90,11 +89,11 @@ export function NamespaceDetails(props) { windowTitle={t('namespaces.overview.title')} customColumns={customColumns} headerActions={headerActions} + customHealthCards={[ + () => , + () => , + ]} > -
- - -
{LimitrangesList} {ResourceQuotasList} {Events} diff --git a/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloads.js b/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloads.js index 535fd27cfa..547988b68f 100644 --- a/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloads.js +++ b/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloads.js @@ -36,44 +36,52 @@ export function NamespaceWorkloads({ namespace }) { return ( <> {(podsData || deploymentsData) && ( -
+ <> {podsData && ( - +
+ +
)} {deploymentsData && ( - +
+ +
)} -
+ )} ); diff --git a/src/resources/Namespaces/ResourcesUsage.js b/src/resources/Namespaces/ResourcesUsage.js index a3eba5bdd6..bb144aa6db 100644 --- a/src/resources/Namespaces/ResourcesUsage.js +++ b/src/resources/Namespaces/ResourcesUsage.js @@ -128,28 +128,34 @@ export const ResourcesUsage = ({ namespace }) => { return ( <> - - } - > - - - - } - > - - +
+ + } + > + + +
+
+ + } + > + + +
); }; diff --git a/src/resources/ReplicaSets/ReplicaSetDetails.js b/src/resources/ReplicaSets/ReplicaSetDetails.js index 92efcf3504..9244203dce 100644 --- a/src/resources/ReplicaSets/ReplicaSetDetails.js +++ b/src/resources/ReplicaSets/ReplicaSetDetails.js @@ -102,27 +102,30 @@ export function ReplicaSetsDetails(props) { ); - const customOverview = resource => { + const ReplicasOverview = resource => { return ( - +
+ +
); }; @@ -140,7 +143,7 @@ export function ReplicaSetsDetails(props) { statusConditions={statusConditions} description={ResourceDescription} createResourceForm={ReplicaSetCreate} - customOverview={customOverview} + customHealthCards={[ReplicasOverview]} {...props} /> ); diff --git a/src/shared/components/CountingCard/CountingCard.tsx b/src/shared/components/CountingCard/CountingCard.tsx index 07033e41a3..61e6e39040 100644 --- a/src/shared/components/CountingCard/CountingCard.tsx +++ b/src/shared/components/CountingCard/CountingCard.tsx @@ -12,22 +12,29 @@ import './CountingCard.scss'; type CountingCardProps = { value: number; - extraInfo: any; + extraInfo: [ExtraInfo]; title: string; subTitle: string; - resourceUrl: string; - isClusterResource: boolean; - className: string; + resourceUrl?: string; + isClusterResource?: boolean; + allNamespaceURL?: boolean; + className?: string; additionalContent?: React.ReactNode; }; +type ExtraInfo = { + value: string; + title: string; +}; + export const CountingCard = ({ value, extraInfo, title, - subTitle = ' ', + subTitle = '', resourceUrl, isClusterResource = false, + allNamespaceURL = true, className = '', additionalContent, }: CountingCardProps) => { @@ -73,9 +80,10 @@ export const CountingCard = ({ url={ isClusterResource ? clusterUrl(resourceUrl) - : namespaceUrl(resourceUrl, { - namespace: '-all-', - }) + : namespaceUrl( + resourceUrl, + allNamespaceURL ? { namespace: '-all-' } : {}, + ) } className="counting-card__link" > diff --git a/src/shared/components/ResourceDetails/ResourceDetails.js b/src/shared/components/ResourceDetails/ResourceDetails.js index db28be6bdd..7cf2d31983 100644 --- a/src/shared/components/ResourceDetails/ResourceDetails.js +++ b/src/shared/components/ResourceDetails/ResourceDetails.js @@ -25,6 +25,7 @@ import { Tooltip } from '../Tooltip/Tooltip'; import YamlUploadDialog from 'resources/Namespaces/YamlUpload/YamlUploadDialog'; import { createPortal } from 'react-dom'; import ResourceDetailsCard from './ResourceDetailsCard'; +import { ResourceHealthCard } from '../ResourceHealthCard/ResourceHealthCard'; import { ResourceStatusCard } from '../ResourceStatusCard/ResourceStatusCard'; import { EMPTY_TEXT_PLACEHOLDER } from '../../constants'; import { ReadableElapsedTimeFromNow } from '../ReadableElapsedTimeFromNow/ReadableElapsedTimeFromNow'; @@ -67,7 +68,7 @@ ResourceDetails.propTypes = { showYamlTab: PropTypes.bool, layoutCloseCreateUrl: PropTypes.string, layoutNumber: PropTypes.string, - customOverviewCard: PropTypes.node, + customHealthCards: PropTypes.node, }; ResourceDetails.defaultProps = { @@ -169,7 +170,7 @@ function Resource({ disableDelete, statusBadge, customStatusColumns, - customOverview, + customHealthCards, statusConditions, headerContent, }) { @@ -394,7 +395,9 @@ function Resource({ /> ); - const customOverviewCard = customOverview && customOverview(resource); + const customOverviewCard = (customHealthCards || []).map(healthCard => + healthCard(resource), + ); return ( @@ -433,9 +436,7 @@ function Resource({ {resourceDetailsCard} {resourceStatusCard && resourceStatusCard} -
- {customOverviewCard && customOverviewCard} -
+ )} }> diff --git a/src/shared/components/ResourceHealthCard/ResourceHealthCard.js b/src/shared/components/ResourceHealthCard/ResourceHealthCard.js new file mode 100644 index 0000000000..ffa40d30d6 --- /dev/null +++ b/src/shared/components/ResourceHealthCard/ResourceHealthCard.js @@ -0,0 +1,24 @@ +import { Title } from '@ui5/webcomponents-react'; +import { useTranslation } from 'react-i18next'; +import { spacing } from '@ui5/webcomponents-react-base'; + +export function ResourceHealthCard({ customHealthCards }) { + const { t } = useTranslation(); + if (!customHealthCards?.length) return null; + return ( + <> + + {t('common.headers.monitoring-and-health')} + +
+ {customHealthCards} +
+ + ); +}