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 && ( -