From 0f3c7b14243ab37def6f92822a7d3e15dbf09a5c Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 8 Mar 2022 14:14:23 +0200 Subject: [PATCH] refactor(home): migrate view to react [EE-1810] (#6314) * refactor(http): parse axios errors (#6325) * refactor(home): use endpoint-list as react component [EE-1814] (#6060) * refactor(home): use endpoint-list as react component fix(home): add missing features and refactors - kubebutton - group name - poll when endpoint is off - state management refactor(endpoints): use stat component fix(endpoints): add space between items refactor(endpoints): move stats to components refactor(endpoints): fetch time refactor(home): move logic refactor(home): move fe render logic refactor(settings): use vanilla js for publicSettings refactor(kube): remove angular from kube config service feat(home): add kubeconfig button feat(home): send analytics when opening kubeconfig modal fix(home): memoize footer refactor(home): use react-query for loading fix(home): show correct control for kubeconfig modal refactor(home): use debounce refactor(home): use new components refactor(home): replace endpoints with environments refactor(home): move endpoint-list component to home fix(home): show group name refactor(home): use switch for environment icon fix(kubeconfig): fix default case refactor(axios): use parse axios error refactor(home): use link components for navigate fix(home): align azure icon refactor(home): refactor stats refactor(home): export envstatusbadge refactor(home): remove unused bindings * chore(home): write tests for edge indicator * chore(home): basic stories for environment item * style(settings): reformat * fix(environments): add publicurl * refactor(home): use table components * refactor(datatables): merge useSearchBarState * refactor(home): fetch group in env item * chore(tests): basic tests * chore(home): test when no envs * refactor(tags): use axios for tagService * refactor(env-groups): use axios for getGroups * feat(app): ui-state context provider * refactor(home): create MotdPanel * refactor(app): create InformationPanel * feat(endpoints): fetch number of total endpoints * refactor(app): merge hooks * refactor(home): migrate view to react [EE-1810] fixes [EE-1810] refactor(home): wip use react view feat(home): show message if no endpoints refactor(home): show endpoint list refactor(home): don't use home to manage link refactor(home): move state refactor(home): check if edge using util refactor(home): move inf panels chore(home): tests refactor(home): load groups and tags in env-item refactor(settings): revert publicSettings change refactor(home): move confirm snapshot method * fix(home): show tags * fix(environments): handle missing snapshots * fix(kube/volumes): fetch pesistent volume claims * refactor(kube): remove use of endpointProvider * refactor(endpoints): set current endpoint * chore(home): add data-cy for tests * chore(tests): mock axios-progress-bar * refactor(home): move use env list to env module * feat(app): sync home view changes with ee * fix(home): sort page header * fix(app): fix tests * chore(github): use yarn cache * refactor(environments): load list of groups * chore(babel): remove auto 18n keys extraction * chore(environments): fix tests * refactor(k8s/application): use current endpoint * fix(app/header): add margin to header * refactor(app): remove unused types * refactor(app): use rq onError handler * refactor(home): wrap element with button --- .github/workflows/lint.yml | 15 +- .github/workflows/test-client.yaml | 10 +- api/http/handler/endpoints/endpoint_list.go | 2 + app/__mocks__/axios-progress-bar.ts | 1 + app/app.js | 2 +- app/assets/css/app.css | 16 +- app/assets/css/vendor-override.css | 27 --- .../ContainersDatatable.tsx | 4 +- .../ContainersDatatableContainer.tsx | 7 +- .../EdgeDevicesDatatable.tsx | 15 +- .../EdgeDevicesDatatableContainer.tsx | 12 +- .../columns/RowContext.tsx | 7 +- .../EdgeDevicesDatatable/columns/group.tsx | 11 +- .../edgeDevicesView/edgeDevicesView.html | 1 + .../edgeDevicesViewController.js | 3 +- .../resourcePoolsDatatable.js | 2 +- .../helm-add-repository.controller.js | 5 +- .../helm/helm-templates/helm-templates.html | 2 +- app/kubernetes/rest/kubeconfig.js | 20 -- app/kubernetes/services/kubeconfig.service.ts | 43 ++++ app/kubernetes/services/kubeconfigService.js | 40 ---- .../services/persistentVolumeClaimService.js | 17 +- app/kubernetes/services/volumeService.js | 14 +- .../create/createApplicationController.js | 7 +- .../edit/applicationController.js | 8 +- .../views/dashboard/dashboardController.js | 3 +- .../views/resource-pools/resourcePools.html | 1 + app/kubernetes/views/volumes/edit/volume.js | 1 + .../views/volumes/edit/volumeController.js | 3 +- .../views/volumes/volumesController.js | 3 +- app/portainer/__module.js | 9 +- app/portainer/components/Button/Button.tsx | 4 +- app/portainer/components/Button/index.ts | 2 - .../InformationPanel/InformationPanel.tsx | 47 ++++ .../InformationPanelAngular.html} | 0 .../InformationPanelAngular.js | 8 + .../components/InformationPanel/index.ts | 3 + .../components/PageHeader/HeaderContainer.css | 2 +- .../PageHeader/HeaderContent.test.tsx | 6 + .../PageHeader/HeaderTitle.test.tsx | 6 + .../PageHeader/PageHeader.module.css | 4 + .../components/PageHeader/PageHeader.tsx | 8 +- .../datatables/components/SearchBar.tsx | 50 ++--- .../datatables/components/TableActions.tsx | 12 +- .../components/datatables/datatable.css | 12 ++ .../endpoint-item/endpoint-item-controller.js | 61 ------ .../endpoint-item/endpointItem.html | 140 ------------ .../endpoint-item/endpointItem.js | 16 -- .../endpoint-list/endpoint-list-controller.js | 185 ---------------- .../components/endpoint-list/endpoint-list.js | 17 -- .../endpoint-list/endpointList.html | 87 -------- .../group-association-table.js | 4 +- app/portainer/components/index.js | 2 + .../information-panel/information-panel.js | 8 - .../components/motd-panel/motd-panel.js | 8 - .../components/motd-panel/motdPanel.html | 26 --- .../environment-groups.service.ts | 35 +++ app/portainer/environment-groups/queries.ts | 29 +++ app/portainer/environment-groups/types.ts | 14 ++ .../environment.service/create.ts | 9 +- .../environments/environment.service/index.ts | 19 +- app/portainer/environments/queries.ts | 46 ++++ app/portainer/environments/types.ts | 22 +- app/portainer/environments/utils.ts | 8 + app/portainer/filters/filters.js | 9 +- app/portainer/filters/index.js | 8 +- app/portainer/helpers/strings.ts | 7 + app/portainer/helpers/tagHelper.js | 10 +- app/portainer/home/BackupFailedPanel.test.tsx | 38 ++++ app/portainer/home/BackupFailedPanel.tsx | 42 ++++ .../home/EdgeLoadingSpinner.module.css | 9 + app/portainer/home/EdgeLoadingSpinner.tsx | 12 ++ .../EnvironmentItem/EdgeIndicator.test.tsx | 34 +++ .../EnvironmentItem/EdgeIndicator.tsx | 55 +++++ .../EnvironmentItem/EnvironmentIcon.tsx | 34 +++ .../EnvironmentItem.module.css | 23 ++ .../EnvironmentItem.stories.tsx | 81 +++++++ .../EnvironmentItem/EnvironmentItem.test.tsx | 74 +++++++ .../EnvironmentItem/EnvironmentItem.tsx | 203 ++++++++++++++++++ .../EnvironmentItem/EnvironmentStats.tsx | 34 +++ .../EnvironmentStatsDocker.tsx | 96 +++++++++ .../EnvironmentItem/EnvironmentStatsItem.tsx | 17 ++ .../EnvironmentStatsKubernetes.tsx | 38 ++++ .../EnvironmentStatusBadge.tsx | 22 ++ .../EnvironmentList/EnvironmentItem/index.ts | 1 + .../EnvironmentList.module.css | 7 + .../EnvironmentList/EnvironmentList.test.tsx | 58 +++++ .../home/EnvironmentList/EnvironmentList.tsx | 159 ++++++++++++++ .../home/EnvironmentList/KubeconfigButton.tsx | 81 +++++++ .../NoEnvironmentsInfoPanel.tsx | 24 +++ app/portainer/home/EnvironmentList/index.ts | 13 ++ app/portainer/home/HomeView.tsx | 88 ++++++++ app/portainer/home/LicenseNodePanel.test.tsx | 48 +++++ app/portainer/home/LicenseNodePanel.tsx | 56 +++++ app/portainer/home/MotdPanel.tsx | 56 +++++ app/portainer/home/home.service.ts | 15 ++ app/portainer/home/index.ts | 9 + app/portainer/home/types.ts | 7 + app/portainer/hooks/UIStateProvider.tsx | 28 +++ app/portainer/hooks/useLocalStorage.ts | 4 +- .../hooks/usePaginationLimitState.ts | 14 ++ app/portainer/hooks/useUser.tsx | 5 + .../license.service.test.ts | 43 ++++ .../license-management/license.service.ts | 130 +++++++++++ app/portainer/license-management/types.ts | 43 ++++ .../license-management/use-license.service.ts | 20 ++ app/portainer/models/motd.js | 7 - app/portainer/rest/backup.js | 1 - app/portainer/rest/motd.js | 17 -- app/portainer/services/api/backup.service.ts | 29 +++ app/portainer/services/api/backupService.js | 14 -- app/portainer/services/api/motdService.js | 27 --- app/portainer/services/api/status.service.ts | 24 +++ app/portainer/services/api/userService.js | 17 +- app/portainer/services/axios.ts | 4 +- app/portainer/services/endpointProvider.js | 16 +- .../services/modal.service/confirm.ts | 15 -- app/portainer/services/modal.service/index.ts | 2 - app/portainer/services/notifications.test.ts | 13 ++ app/portainer/settings/settings.service.ts | 28 +++ app/portainer/tags/queries.ts | 18 ++ app/portainer/tags/tags.service.ts | 38 ++++ app/portainer/tags/types.ts | 6 + app/portainer/views/home/home.html | 62 ------ app/portainer/views/home/homeController.js | 102 --------- app/react-tools/RootProvider.tsx | 9 +- app/setup-tests/server-handlers.ts | 43 ++++ babel.config.js | 2 +- package.json | 3 + yarn.lock | 15 ++ 130 files changed, 2400 insertions(+), 1078 deletions(-) create mode 100644 app/__mocks__/axios-progress-bar.ts delete mode 100644 app/kubernetes/rest/kubeconfig.js create mode 100644 app/kubernetes/services/kubeconfig.service.ts delete mode 100644 app/kubernetes/services/kubeconfigService.js create mode 100644 app/portainer/components/InformationPanel/InformationPanel.tsx rename app/portainer/components/{information-panel/informationPanel.html => InformationPanel/InformationPanelAngular.html} (100%) create mode 100644 app/portainer/components/InformationPanel/InformationPanelAngular.js create mode 100644 app/portainer/components/InformationPanel/index.ts create mode 100644 app/portainer/components/PageHeader/PageHeader.module.css delete mode 100644 app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js delete mode 100644 app/portainer/components/endpoint-list/endpoint-item/endpointItem.html delete mode 100644 app/portainer/components/endpoint-list/endpoint-item/endpointItem.js delete mode 100644 app/portainer/components/endpoint-list/endpoint-list-controller.js delete mode 100644 app/portainer/components/endpoint-list/endpoint-list.js delete mode 100644 app/portainer/components/endpoint-list/endpointList.html delete mode 100644 app/portainer/components/information-panel/information-panel.js delete mode 100644 app/portainer/components/motd-panel/motd-panel.js delete mode 100644 app/portainer/components/motd-panel/motdPanel.html create mode 100644 app/portainer/environment-groups/environment-groups.service.ts create mode 100644 app/portainer/environment-groups/queries.ts create mode 100644 app/portainer/environment-groups/types.ts create mode 100644 app/portainer/environments/queries.ts create mode 100644 app/portainer/helpers/strings.ts create mode 100644 app/portainer/home/BackupFailedPanel.test.tsx create mode 100644 app/portainer/home/BackupFailedPanel.tsx create mode 100644 app/portainer/home/EdgeLoadingSpinner.module.css create mode 100644 app/portainer/home/EdgeLoadingSpinner.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EdgeIndicator.test.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EdgeIndicator.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.module.css create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStats.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsItem.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsKubernetes.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatusBadge.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentItem/index.ts create mode 100644 app/portainer/home/EnvironmentList/EnvironmentList.module.css create mode 100644 app/portainer/home/EnvironmentList/EnvironmentList.test.tsx create mode 100644 app/portainer/home/EnvironmentList/EnvironmentList.tsx create mode 100644 app/portainer/home/EnvironmentList/KubeconfigButton.tsx create mode 100644 app/portainer/home/EnvironmentList/NoEnvironmentsInfoPanel.tsx create mode 100644 app/portainer/home/EnvironmentList/index.ts create mode 100644 app/portainer/home/HomeView.tsx create mode 100644 app/portainer/home/LicenseNodePanel.test.tsx create mode 100644 app/portainer/home/LicenseNodePanel.tsx create mode 100644 app/portainer/home/MotdPanel.tsx create mode 100644 app/portainer/home/home.service.ts create mode 100644 app/portainer/home/index.ts create mode 100644 app/portainer/home/types.ts create mode 100644 app/portainer/hooks/UIStateProvider.tsx create mode 100644 app/portainer/hooks/usePaginationLimitState.ts create mode 100644 app/portainer/license-management/license.service.test.ts create mode 100644 app/portainer/license-management/license.service.ts create mode 100644 app/portainer/license-management/types.ts create mode 100644 app/portainer/license-management/use-license.service.ts delete mode 100644 app/portainer/models/motd.js delete mode 100644 app/portainer/rest/motd.js create mode 100644 app/portainer/services/api/backup.service.ts delete mode 100644 app/portainer/services/api/motdService.js create mode 100644 app/portainer/services/api/status.service.ts create mode 100644 app/portainer/settings/settings.service.ts create mode 100644 app/portainer/tags/queries.ts create mode 100644 app/portainer/tags/tags.service.ts create mode 100644 app/portainer/tags/types.ts delete mode 100644 app/portainer/views/home/home.html delete mode 100644 app/portainer/views/home/homeController.js diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b6b7af587..08899173e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,17 +18,12 @@ jobs: runs-on: ubuntu-latest steps: - - name: Check out Git repository - uses: actions/checkout@v2 - - - name: Set up Node.js - uses: actions/setup-node@v1 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 with: - node-version: 12 - - # ESLint and Prettier must be in `package.json` - - name: Install Node.js dependencies - run: yarn --frozen-lockfile + node-version: '14' + cache: 'yarn' + - run: yarn --frozen-lockfile - name: Run linters uses: wearerequired/lint-action@v1 diff --git a/.github/workflows/test-client.yaml b/.github/workflows/test-client.yaml index 139574b1b..085a7512e 100644 --- a/.github/workflows/test-client.yaml +++ b/.github/workflows/test-client.yaml @@ -1,11 +1,15 @@ name: Test Frontend on: push jobs: - build: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Install modules - run: yarn --frozen-lockfile + - uses: actions/setup-node@v2 + with: + node-version: '14' + cache: 'yarn' + - run: yarn install --frozen-lockfile + - name: Run tests run: yarn test:client diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 00107278c..ddede499f 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -80,6 +80,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht } filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext) + totalAvailableEndpoints := len(filteredEndpoints) if endpointIDs != nil { filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs) @@ -127,6 +128,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht } w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount)) + w.Header().Set("X-Total-Available", strconv.Itoa(totalAvailableEndpoints)) return response.JSON(w, paginatedEndpoints) } diff --git a/app/__mocks__/axios-progress-bar.ts b/app/__mocks__/axios-progress-bar.ts new file mode 100644 index 000000000..2b3d7f6b4 --- /dev/null +++ b/app/__mocks__/axios-progress-bar.ts @@ -0,0 +1 @@ +export function loadProgressBar() {} diff --git a/app/app.js b/app/app.js index 10996f0d8..0443e8ddf 100644 --- a/app/app.js +++ b/app/app.js @@ -38,7 +38,7 @@ export function onStartupAngular($rootScope, $state, $interval, LocalStorage, En function ping(EndpointProvider, SystemService) { const endpoint = EndpointProvider.currentEndpoint(); - if (endpoint !== undefined && endpoint.Type == PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) { + if (endpoint && endpoint.Type == PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) { SystemService.ping(endpoint.Id); } } diff --git a/app/assets/css/app.css b/app/assets/css/app.css index 158289a19..f014936e2 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -375,10 +375,6 @@ a[ng-click] { background-color: var(--white-color) fff; } -.pagination-controls { - margin-left: 10px; -} - .user-box { margin-right: 25px; } @@ -832,6 +828,18 @@ json-tree .branch-preview { align-items: center; } +.space-x-2 > * + * { + margin-left: 0.5rem; +} + +.space-x-3 > * + * { + margin-left: 0.75rem; +} + +.space-x-4 > * + * { + margin-left: 1rem; +} + .space-y-8 > * + * { margin-top: 2rem; } diff --git a/app/assets/css/vendor-override.css b/app/assets/css/vendor-override.css index 646c4fcf2..65adf7942 100644 --- a/app/assets/css/vendor-override.css +++ b/app/assets/css/vendor-override.css @@ -222,33 +222,6 @@ json-tree .branch-preview { background-color: var(--bg-progress-color); } -.pagination > .disabled > span, -.pagination > .disabled > span:hover, -.pagination > .disabled > span:focus, -.pagination > .disabled > a, -.pagination > .disabled > a:hover, -.pagination > .disabled > a:focus { - color: var(--text-pagination-color); - background-color: var(--bg-pagination-color); - border-color: var(--border-pagination-color); -} - -.pagination > li > a, -.pagination > li > span { - background-color: var(--bg-pagination-span-color); - border-color: var(--border-pagination-span-color); - color: var(--text-pagination-span-color); -} - -.pagination > li > a:hover, -.pagination > li > span:hover, -.pagination > li > a:focus, -.pagination > li > span:focus { - background-color: var(--bg-pagination-hover-color); - border-color: var(--border-pagination-hover-color); - color: var(--text-pagination-span-hover-color); -} - .ui-select-bootstrap .ui-select-choices-row > span { color: var(--text-ui-select-color); } diff --git a/app/docker/containers/components/ContainersDatatable/ContainersDatatable.tsx b/app/docker/containers/components/ContainersDatatable/ContainersDatatable.tsx index 0e8ef9cdf..09beab68b 100644 --- a/app/docker/containers/components/ContainersDatatable/ContainersDatatable.tsx +++ b/app/docker/containers/components/ContainersDatatable/ContainersDatatable.tsx @@ -30,8 +30,8 @@ import { ColumnVisibilityMenu } from '@/portainer/components/datatables/componen import { useRepeater } from '@/portainer/components/datatables/components/useRepeater'; import { useDebounce } from '@/portainer/hooks/useDebounce'; import { - useSearchBarContext, SearchBar, + useSearchBarState, } from '@/portainer/components/datatables/components/SearchBar'; import type { ContainersTableSettings, @@ -63,7 +63,7 @@ export function ContainersDatatable({ }: ContainerTableProps) { const { settings, setTableSettings } = useTableSettings(); - const [searchBarValue, setSearchBarValue] = useSearchBarContext(); + const [searchBarValue, setSearchBarValue] = useSearchBarState('containers'); const columns = useColumns(); diff --git a/app/docker/containers/components/ContainersDatatable/ContainersDatatableContainer.tsx b/app/docker/containers/components/ContainersDatatable/ContainersDatatableContainer.tsx index 3b2dae099..03dbc678b 100644 --- a/app/docker/containers/components/ContainersDatatable/ContainersDatatableContainer.tsx +++ b/app/docker/containers/components/ContainersDatatable/ContainersDatatableContainer.tsx @@ -1,7 +1,6 @@ import { react2angular } from '@/react-tools/react2angular'; import { EnvironmentProvider } from '@/portainer/environments/useEnvironment'; import { TableSettingsProvider } from '@/portainer/components/datatables/components/useTableSettings'; -import { SearchBarProvider } from '@/portainer/components/datatables/components/SearchBar'; import type { Environment } from '@/portainer/environments/types'; import { @@ -30,10 +29,8 @@ export function ContainersDatatableContainer({ return ( - - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - - + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + ); diff --git a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx b/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx index 01dbd97a6..33476e8da 100644 --- a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx +++ b/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx @@ -8,6 +8,7 @@ import { usePagination, } from 'react-table'; import { useRowSelectColumn } from '@lineup-lite/hooks'; +import _ from 'lodash'; import { Environment } from '@/portainer/environments/types'; import { PaginationControls } from '@/portainer/components/pagination-controls'; @@ -27,7 +28,7 @@ import { ColumnVisibilityMenu } from '@/portainer/components/datatables/componen import { useRepeater } from '@/portainer/components/datatables/components/useRepeater'; import { useDebounce } from '@/portainer/hooks/useDebounce'; import { - useSearchBarContext, + useSearchBarState, SearchBar, } from '@/portainer/components/datatables/components/SearchBar'; import { useRowSelect } from '@/portainer/components/datatables/components/useRowSelect'; @@ -38,34 +39,39 @@ import { EdgeDevicesDatatableSettings } from '@/edge/devices/components/EdgeDevi import { EdgeDevicesDatatableActions } from '@/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableActions'; import { AMTDevicesDatatable } from '@/edge/devices/components/AMTDevicesDatatable/AMTDevicesDatatable'; import { TextTip } from '@/portainer/components/Tip/TextTip'; +import { EnvironmentGroup } from '@/portainer/environment-groups/types'; import { RowProvider } from './columns/RowContext'; import { useColumns } from './columns'; import styles from './EdgeDevicesDatatable.module.css'; export interface EdgeDevicesTableProps { + storageKey: string; isEnabled: boolean; isFdoEnabled: boolean; isOpenAmtEnabled: boolean; disableTrustOnFirstConnect: boolean; mpsServer: string; dataset: Environment[]; + groups: EnvironmentGroup[]; onRefresh(): Promise; setLoadingMessage(message: string): void; } export function EdgeDevicesDatatable({ + storageKey, isFdoEnabled, isOpenAmtEnabled, disableTrustOnFirstConnect, mpsServer, dataset, + groups, onRefresh, setLoadingMessage, }: EdgeDevicesTableProps) { const { settings, setTableSettings } = useTableSettings(); - const [searchBarValue, setSearchBarValue] = useSearchBarContext(); + const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey); const columns = useColumns(); @@ -131,6 +137,8 @@ export function EdgeDevicesDatatable({ environment.AMTDeviceGUID && environment.AMTDeviceGUID !== '' ); + const groupsById = _.groupBy(groups, 'Id'); + return ( @@ -201,12 +209,13 @@ export function EdgeDevicesDatatable({ {page.map((row) => { prepareRow(row); const { key, className, role, style } = row.getRowProps(); - + const group = groupsById[row.original.GroupId]; return ( cells={row.cells} diff --git a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx b/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx index 83d28db75..0fde89a25 100644 --- a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx +++ b/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx @@ -1,6 +1,5 @@ import { react2angular } from '@/react-tools/react2angular'; import { TableSettingsProvider } from '@/portainer/components/datatables/components/useTableSettings'; -import { SearchBarProvider } from '@/portainer/components/datatables/components/SearchBar'; import { EdgeDevicesDatatable, @@ -18,12 +17,12 @@ export function EdgeDevicesDatatableContainer({ sortBy: { id: 'state', desc: false }, }; + const storageKey = 'edgeDevices'; + return ( - - - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - - + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + ); } @@ -31,6 +30,7 @@ export function EdgeDevicesDatatableContainer({ export const EdgeDevicesDatatableAngular = react2angular( EdgeDevicesDatatableContainer, [ + 'groups', 'dataset', 'onRefresh', 'setLoadingMessage', diff --git a/app/edge/devices/components/EdgeDevicesDatatable/columns/RowContext.tsx b/app/edge/devices/components/EdgeDevicesDatatable/columns/RowContext.tsx index bd81f0e89..808a1bb6c 100644 --- a/app/edge/devices/components/EdgeDevicesDatatable/columns/RowContext.tsx +++ b/app/edge/devices/components/EdgeDevicesDatatable/columns/RowContext.tsx @@ -3,23 +3,26 @@ import { createContext, useContext, useMemo, PropsWithChildren } from 'react'; interface RowContextState { disableTrustOnFirstConnect: boolean; isOpenAmtEnabled: boolean; + groupName?: string; } const RowContext = createContext(null); export interface RowProviderProps { disableTrustOnFirstConnect: boolean; + groupName?: string; isOpenAmtEnabled: boolean; } export function RowProvider({ disableTrustOnFirstConnect, + groupName, isOpenAmtEnabled, children, }: PropsWithChildren) { const state = useMemo( - () => ({ disableTrustOnFirstConnect, isOpenAmtEnabled }), - [disableTrustOnFirstConnect, isOpenAmtEnabled] + () => ({ disableTrustOnFirstConnect, groupName, isOpenAmtEnabled }), + [disableTrustOnFirstConnect, groupName, isOpenAmtEnabled] ); return {children}; diff --git a/app/edge/devices/components/EdgeDevicesDatatable/columns/group.tsx b/app/edge/devices/components/EdgeDevicesDatatable/columns/group.tsx index 194caab3b..d876d2116 100644 --- a/app/edge/devices/components/EdgeDevicesDatatable/columns/group.tsx +++ b/app/edge/devices/components/EdgeDevicesDatatable/columns/group.tsx @@ -3,10 +3,19 @@ import { Column } from 'react-table'; import { Environment } from '@/portainer/environments/types'; import { DefaultFilter } from '@/portainer/components/datatables/components/Filter'; +import { useRowContext } from './RowContext'; + export const group: Column = { Header: 'Group', - accessor: (row) => row.GroupName || '-', + accessor: (row) => row.GroupId, + Cell: GroupCell, id: 'groupName', Filter: DefaultFilter, canHide: true, }; + +function GroupCell() { + const { groupName } = useRowContext(); + + return groupName; +} diff --git a/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesView.html b/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesView.html index ae55a784b..752b76d81 100644 --- a/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesView.html +++ b/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesView.html @@ -28,6 +28,7 @@
{ try { const [endpointsResponse, groups] = await Promise.all([getEndpoints(0, 100, { types: [EnvironmentType.EdgeAgentOnDocker] }), GroupService.groups()]); - EndpointHelper.mapGroupNameToEndpoint(endpointsResponse.value, groups); + ctrl.groups = groups; ctrl.edgeDevices = endpointsResponse.value; } catch (err) { Notifications.error('Failure', err, 'Unable to retrieve edge devices'); diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js index 520b50a8a..aacd0c3c4 100644 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js @@ -2,6 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatab templateUrl: './resourcePoolsDatatable.html', controller: 'KubernetesResourcePoolsDatatableController', bindings: { + endpoint: '<', titleText: '@', titleIcon: '@', dataset: '<', @@ -10,6 +11,5 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatab reverseOrder: '<', removeAction: '<', refreshCallback: '<', - endpoint: '<', }, }); diff --git a/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.controller.js b/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.controller.js index ac9ba546f..f8c762a87 100644 --- a/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.controller.js +++ b/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.controller.js @@ -1,11 +1,10 @@ export default class HelmAddRepositoryController { /* @ngInject */ - constructor($state, $async, HelmService, Notifications, EndpointProvider) { + constructor($state, $async, HelmService, Notifications) { this.$state = $state; this.$async = $async; this.HelmService = HelmService; this.Notifications = Notifications; - this.EndpointProvider = EndpointProvider; } doesRepoExist() { @@ -19,7 +18,7 @@ export default class HelmAddRepositoryController { async addRepository() { this.state.isAddingRepo = true; try { - await this.HelmService.addHelmRepository(this.EndpointProvider.currentEndpoint().Id, { url: this.state.repository }); + await this.HelmService.addHelmRepository(this.endpoint.Id, { url: this.state.repository }); this.Notifications.success('Helm repository added successfully'); this.$state.reload(this.$state.current); } catch (err) { diff --git a/app/kubernetes/components/helm/helm-templates/helm-templates.html b/app/kubernetes/components/helm/helm-templates/helm-templates.html index 3ccc2ead0..389ad54d3 100644 --- a/app/kubernetes/components/helm/helm-templates/helm-templates.html +++ b/app/kubernetes/components/helm/helm-templates/helm-templates.html @@ -146,7 +146,7 @@
- +
diff --git a/app/kubernetes/rest/kubeconfig.js b/app/kubernetes/rest/kubeconfig.js deleted file mode 100644 index 5db0930ca..000000000 --- a/app/kubernetes/rest/kubeconfig.js +++ /dev/null @@ -1,20 +0,0 @@ -import angular from 'angular'; - -angular.module('portainer.kubernetes').factory('KubernetesConfig', KubernetesConfigFactory); - -/* @ngInject */ -function KubernetesConfigFactory($http, EndpointProvider, API_ENDPOINT_KUBERNETES) { - return { get }; - - async function get(environmentIDs) { - return $http({ - method: 'GET', - url: `${API_ENDPOINT_KUBERNETES}/config`, - params: { ids: JSON.stringify(environmentIDs.map((x) => parseInt(x))) }, - responseType: 'blob', - headers: { - Accept: 'text/yaml', - }, - }); - } -} diff --git a/app/kubernetes/services/kubeconfig.service.ts b/app/kubernetes/services/kubeconfig.service.ts new file mode 100644 index 000000000..92e8cfd99 --- /dev/null +++ b/app/kubernetes/services/kubeconfig.service.ts @@ -0,0 +1,43 @@ +import { saveAs } from 'file-saver'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { EnvironmentId } from '@/portainer/environments/types'; +import { publicSettings } from '@/portainer/settings/settings.service'; + +const baseUrl = 'kubernetes'; + +export async function downloadKubeconfigFile(environmentIds: EnvironmentId[]) { + try { + const { headers, data } = await axios.get(`${baseUrl}/config`, { + params: { ids: JSON.stringify(environmentIds) }, + responseType: 'blob', + headers: { + Accept: 'text/yaml', + }, + }); + const contentDispositionHeader = headers['content-disposition']; + const filename = contentDispositionHeader.replace('attachment;', '').trim(); + saveAs(data, filename); + } catch (e) { + throw parseAxiosError(e as Error, ''); + } +} + +export async function expiryMessage() { + const settings = await publicSettings(); + + const prefix = 'Kubeconfig file will'; + switch (settings.KubeconfigExpiry) { + case '24h': + return `${prefix} expire in 1 day.`; + case '168h': + return `${prefix} expire in 7 days.`; + case '720h': + return `${prefix} expire in 30 days.`; + case '8640h': + return `${prefix} expire in 1 year.`; + case '0': + default: + return `${prefix} not expire.`; + } +} diff --git a/app/kubernetes/services/kubeconfigService.js b/app/kubernetes/services/kubeconfigService.js deleted file mode 100644 index ed277bc6e..000000000 --- a/app/kubernetes/services/kubeconfigService.js +++ /dev/null @@ -1,40 +0,0 @@ -import angular from 'angular'; - -class KubernetesConfigService { - /* @ngInject */ - constructor(KubernetesConfig, FileSaver, SettingsService) { - this.KubernetesConfig = KubernetesConfig; - this.FileSaver = FileSaver; - this.SettingsService = SettingsService; - } - - async downloadKubeconfigFile(environmentIDs) { - const response = await this.KubernetesConfig.get(environmentIDs); - const headers = response.headers(); - const contentDispositionHeader = headers['content-disposition']; - const filename = contentDispositionHeader.replace('attachment;', '').trim(); - return this.FileSaver.saveAs(response.data, filename); - } - - async expiryMessage() { - const settings = await this.SettingsService.publicSettings(); - const expiryDays = settings.KubeconfigExpiry; - const prefix = 'Kubeconfig file will '; - switch (expiryDays) { - case '0': - return prefix + 'not expire.'; - case '24h': - return prefix + 'expire in 1 day.'; - case '168h': - return prefix + 'expire in 7 days.'; - case '720h': - return prefix + 'expire in 30 days.'; - case '8640h': - return prefix + 'expire in 1 year.'; - } - return ''; - } -} - -export default KubernetesConfigService; -angular.module('portainer.kubernetes').service('KubernetesConfigService', KubernetesConfigService); diff --git a/app/kubernetes/services/persistentVolumeClaimService.js b/app/kubernetes/services/persistentVolumeClaimService.js index a19fcdf0d..e95c6d1b8 100644 --- a/app/kubernetes/services/persistentVolumeClaimService.js +++ b/app/kubernetes/services/persistentVolumeClaimService.js @@ -6,9 +6,8 @@ import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; class KubernetesPersistentVolumeClaimService { /* @ngInject */ - constructor($async, EndpointProvider, KubernetesPersistentVolumeClaims) { + constructor($async, KubernetesPersistentVolumeClaims) { this.$async = $async; - this.EndpointProvider = EndpointProvider; this.KubernetesPersistentVolumeClaims = KubernetesPersistentVolumeClaims; this.getAsync = this.getAsync.bind(this); @@ -18,7 +17,7 @@ class KubernetesPersistentVolumeClaimService { this.deleteAsync = this.deleteAsync.bind(this); } - async getAsync(namespace, name) { + async getAsync(namespace, storageClasses, name) { try { const params = new KubernetesCommonParams(); params.id = name; @@ -26,28 +25,28 @@ class KubernetesPersistentVolumeClaimService { this.KubernetesPersistentVolumeClaims(namespace).get(params).$promise, this.KubernetesPersistentVolumeClaims(namespace).getYaml(params).$promise, ]); - const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses; + return KubernetesPersistentVolumeClaimConverter.apiToPersistentVolumeClaim(raw, storageClasses, yaml); } catch (err) { throw new PortainerError('Unable to retrieve persistent volume claim', err); } } - async getAllAsync(namespace) { + async getAllAsync(namespace, storageClasses) { try { const data = await this.KubernetesPersistentVolumeClaims(namespace).get().$promise; - const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses; + return _.map(data.items, (item) => KubernetesPersistentVolumeClaimConverter.apiToPersistentVolumeClaim(item, storageClasses)); } catch (err) { throw new PortainerError('Unable to retrieve persistent volume claims', err); } } - get(namespace, name) { + get(namespace, storageClasses, name) { if (name) { - return this.$async(this.getAsync, namespace, name); + return this.$async(this.getAsync, namespace, storageClasses, name); } - return this.$async(this.getAllAsync, namespace); + return this.$async(this.getAllAsync, namespace, storageClasses); } /** diff --git a/app/kubernetes/services/volumeService.js b/app/kubernetes/services/volumeService.js index 7b00aa6e2..916b91968 100644 --- a/app/kubernetes/services/volumeService.js +++ b/app/kubernetes/services/volumeService.js @@ -19,28 +19,28 @@ class KubernetesVolumeService { /** * GET */ - async getAsync(namespace, name) { - const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]); + async getAsync(namespace, storageClasses, name) { + const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, storageClasses, name), this.KubernetesResourcePoolService.get(namespace)]); return KubernetesVolumeConverter.pvcToVolume(pvc, pool); } - async getAllAsync(namespace) { + async getAllAsync(namespace, storageClasses) { const data = await this.KubernetesResourcePoolService.get(namespace); const pools = data instanceof Array ? data : [data]; const res = await Promise.all( _.map(pools, async (pool) => { - const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name); + const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name, storageClasses); return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool)); }) ); return _.flatten(res); } - get(namespace, name) { + get(namespace, storageClasses, name) { if (name) { - return this.$async(this.getAsync, namespace, name); + return this.$async(this.getAsync, namespace, storageClasses, name); } - return this.$async(this.getAllAsync, namespace); + return this.$async(this.getAllAsync, namespace, storageClasses); } /** diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 15f41a3d9..911ceb561 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -932,7 +932,8 @@ class KubernetesCreateApplicationController { refreshVolumes(namespace) { return this.$async(async () => { try { - const volumes = await this.KubernetesVolumeService.get(namespace); + const storageClasses = this.endpoint.Kubernetes.Configuration.StorageClasses; + const volumes = await this.KubernetesVolumeService.get(namespace, storageClasses); _.forEach(volumes, (volume) => { volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, this.applications); }); @@ -1045,9 +1046,11 @@ class KubernetesCreateApplicationController { return this.$async(async () => { try { const namespace = this.$state.params.namespace; + const storageClasses = this.endpoint.Kubernetes.Configuration.StorageClasses; + [this.application, this.persistentVolumeClaims] = await Promise.all([ this.KubernetesApplicationService.get(namespace, this.$state.params.name), - this.KubernetesPersistentVolumeClaimService.get(namespace), + this.KubernetesPersistentVolumeClaimService.get(namespace, storageClasses), ]); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve application details'); diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index c41950860..9d2c8b78f 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -348,12 +348,6 @@ class KubernetesApplicationController { } async onInit() { - const endpointId = this.LocalStorage.getEndpointID(); - const endpoints = this.LocalStorage.getEndpoints(); - const endpoint = _.find(endpoints, function (item) { - return item.Id === endpointId; - }); - this.state = { activeTab: 0, currentName: this.$state.$current.name, @@ -372,7 +366,7 @@ class KubernetesApplicationController { expandedNote: false, useIngress: false, useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics, - publicUrl: endpoint.PublicURL, + publicUrl: this.endpoint.PublicURL, }; this.state.activeTab = this.LocalStorage.getActiveTab('application'); diff --git a/app/kubernetes/views/dashboard/dashboardController.js b/app/kubernetes/views/dashboard/dashboardController.js index 8ec79a0b4..d3ecc3183 100644 --- a/app/kubernetes/views/dashboard/dashboardController.js +++ b/app/kubernetes/views/dashboard/dashboardController.js @@ -33,13 +33,14 @@ class KubernetesDashboardController { async getAllAsync() { const isAdmin = this.Authentication.isAdmin(); + const storageClasses = this.endpoint.Kubernetes.Configuration.StorageClasses; try { const [pools, applications, configurations, volumes, tags] = await Promise.all([ this.KubernetesResourcePoolService.get(), this.KubernetesApplicationService.get(), this.KubernetesConfigurationService.get(), - this.KubernetesVolumeService.get(), + this.KubernetesVolumeService.get(undefined, storageClasses), this.TagService.tags(), ]); this.applications = applications; diff --git a/app/kubernetes/views/resource-pools/resourcePools.html b/app/kubernetes/views/resource-pools/resourcePools.html index 10043b953..b113a9b85 100644 --- a/app/kubernetes/views/resource-pools/resourcePools.html +++ b/app/kubernetes/views/resource-pools/resourcePools.html @@ -6,6 +6,7 @@
{ try { const endpointId = +$transition$.params().endpointId; @@ -85,6 +87,8 @@ angular return; } + EndpointProvider.setCurrentEndpoint(endpoint); + return endpoint; } catch (e) { Notifications.error('Failed loading environment', e); @@ -322,8 +326,7 @@ angular url: '/home', views: { 'content@': { - templateUrl: './views/home/home.html', - controller: 'HomeController', + component: 'homeView', }, }, }; diff --git a/app/portainer/components/Button/Button.tsx b/app/portainer/components/Button/Button.tsx index 0c1b033af..ec6250430 100644 --- a/app/portainer/components/Button/Button.tsx +++ b/app/portainer/components/Button/Button.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from 'react'; +import { MouseEventHandler, PropsWithChildren } from 'react'; import clsx from 'clsx'; type Type = 'submit' | 'button' | 'reset'; @@ -13,7 +13,7 @@ export interface Props { className?: string; dataCy?: string; type?: Type; - onClick?: () => void; + onClick?: MouseEventHandler; } export function Button({ diff --git a/app/portainer/components/Button/index.ts b/app/portainer/components/Button/index.ts index 1ba16921f..216ec6bfc 100644 --- a/app/portainer/components/Button/index.ts +++ b/app/portainer/components/Button/index.ts @@ -3,5 +3,3 @@ import { AddButton } from './AddButton'; import { ButtonGroup } from './ButtonGroup'; export { Button, AddButton, ButtonGroup }; - -export default Button; diff --git a/app/portainer/components/InformationPanel/InformationPanel.tsx b/app/portainer/components/InformationPanel/InformationPanel.tsx new file mode 100644 index 000000000..d38863cb4 --- /dev/null +++ b/app/portainer/components/InformationPanel/InformationPanel.tsx @@ -0,0 +1,47 @@ +import { PropsWithChildren } from 'react'; + +import { Button } from '../Button'; +import { Widget, WidgetBody } from '../widget'; + +interface Props { + title: string; + onDismiss?(): void; + bodyClassName?: string; + wrapperStyle?: Record; +} + +export function InformationPanel({ + title, + onDismiss, + wrapperStyle, + bodyClassName, + children, +}: PropsWithChildren) { + return ( +
+
+ + +
+
+ {title} + {!!onDismiss && ( + + + + )} +
+
{children}
+
+
+
+
+
+ ); +} diff --git a/app/portainer/components/information-panel/informationPanel.html b/app/portainer/components/InformationPanel/InformationPanelAngular.html similarity index 100% rename from app/portainer/components/information-panel/informationPanel.html rename to app/portainer/components/InformationPanel/InformationPanelAngular.html diff --git a/app/portainer/components/InformationPanel/InformationPanelAngular.js b/app/portainer/components/InformationPanel/InformationPanelAngular.js new file mode 100644 index 000000000..acbf9e7c1 --- /dev/null +++ b/app/portainer/components/InformationPanel/InformationPanelAngular.js @@ -0,0 +1,8 @@ +export const InformationPanelAngular = { + templateUrl: './InformationPanelAngular.html', + bindings: { + titleText: '@', + dismissAction: '&?', + }, + transclude: true, +}; diff --git a/app/portainer/components/InformationPanel/index.ts b/app/portainer/components/InformationPanel/index.ts new file mode 100644 index 000000000..95218ede0 --- /dev/null +++ b/app/portainer/components/InformationPanel/index.ts @@ -0,0 +1,3 @@ +export { InformationPanel } from './InformationPanel'; + +export { InformationPanelAngular } from './InformationPanelAngular'; diff --git a/app/portainer/components/PageHeader/HeaderContainer.css b/app/portainer/components/PageHeader/HeaderContainer.css index 08bc8c123..921c89b7c 100644 --- a/app/portainer/components/PageHeader/HeaderContainer.css +++ b/app/portainer/components/PageHeader/HeaderContainer.css @@ -7,7 +7,7 @@ body.hamburg .row.header .meta { } .row.header { - height: 60px; + min-height: 60px; background: var(--bg-row-header-color); margin-bottom: 15px; } diff --git a/app/portainer/components/PageHeader/HeaderContent.test.tsx b/app/portainer/components/PageHeader/HeaderContent.test.tsx index c1bf7fab1..28402476e 100644 --- a/app/portainer/components/PageHeader/HeaderContent.test.tsx +++ b/app/portainer/components/PageHeader/HeaderContent.test.tsx @@ -6,11 +6,17 @@ import { HeaderContainer } from './HeaderContainer'; import { HeaderContent } from './HeaderContent'; test('should not render without a wrapping HeaderContainer', async () => { + const consoleErrorFn = jest + .spyOn(console, 'error') + .mockImplementation(() => jest.fn()); + function renderComponent() { return render(); } expect(renderComponent).toThrowErrorMatchingSnapshot(); + + consoleErrorFn.mockRestore(); }); test('should display a HeaderContent', async () => { diff --git a/app/portainer/components/PageHeader/HeaderTitle.test.tsx b/app/portainer/components/PageHeader/HeaderTitle.test.tsx index e021f1f44..ca4374c8c 100644 --- a/app/portainer/components/PageHeader/HeaderTitle.test.tsx +++ b/app/portainer/components/PageHeader/HeaderTitle.test.tsx @@ -6,12 +6,18 @@ import { HeaderContainer } from './HeaderContainer'; import { HeaderTitle } from './HeaderTitle'; test('should not render without a wrapping HeaderContainer', async () => { + const consoleErrorFn = jest + .spyOn(console, 'error') + .mockImplementation(() => jest.fn()); + const title = 'title'; function renderComponent() { return render(); } expect(renderComponent).toThrowErrorMatchingSnapshot(); + + consoleErrorFn.mockRestore(); }); test('should display a HeaderTitle', async () => { diff --git a/app/portainer/components/PageHeader/PageHeader.module.css b/app/portainer/components/PageHeader/PageHeader.module.css new file mode 100644 index 000000000..bc4eb823d --- /dev/null +++ b/app/portainer/components/PageHeader/PageHeader.module.css @@ -0,0 +1,4 @@ +.reloadButton { + padding: 0; + margin: 0; +} diff --git a/app/portainer/components/PageHeader/PageHeader.tsx b/app/portainer/components/PageHeader/PageHeader.tsx index 8818a5e3d..e42432d3a 100644 --- a/app/portainer/components/PageHeader/PageHeader.tsx +++ b/app/portainer/components/PageHeader/PageHeader.tsx @@ -7,6 +7,7 @@ import { Crumb } from './Breadcrumbs/Breadcrumbs'; import { HeaderContainer } from './HeaderContainer'; import { HeaderContent } from './HeaderContent'; import { HeaderTitle } from './HeaderTitle'; +import styles from './PageHeader.module.css'; interface Props { reload?: boolean; @@ -20,7 +21,12 @@ export function PageHeader({ title, breadcrumbs = [], reload }: Props) { {reload && ( -