diff --git a/app/docker/__module.js b/app/docker/__module.js index 21cfc5149..5756fcdf1 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -603,7 +603,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([ url: '/registries', views: { 'content@': { - component: 'endpointRegistriesView', + component: 'environmentRegistriesView', }, }, data: { @@ -616,7 +616,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([ url: '/registries', views: { 'content@': { - component: 'endpointRegistriesView', + component: 'environmentRegistriesView', }, }, data: { diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index 68af4628b..e4fa9fb6a 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -518,7 +518,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo url: '/registries', views: { 'content@': { - component: 'endpointRegistriesView', + component: 'environmentRegistriesView', }, }, data: { diff --git a/app/portainer/__module.js b/app/portainer/__module.js index b07c557e5..90abd8f90 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -356,8 +356,7 @@ angular url: '/registries', views: { 'content@': { - templateUrl: './views/registries/registries.html', - controller: 'RegistriesController', + component: 'registriesView', }, }, data: { diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html deleted file mode 100644 index f5c5ba9f3..000000000 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html +++ /dev/null @@ -1,145 +0,0 @@ -
- - -
-
-
- -
- {{ $ctrl.titleText }} -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - -
-
- - - -
- - - - - - - -
- - - - - {{ item.Name }} - {{ item.Name }} - authentication-enabled - - {{ item.URL }} - - - - Browse - - - - -
Loading...
-
- -
-
-
diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js deleted file mode 100644 index e5acb6907..000000000 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js +++ /dev/null @@ -1,16 +0,0 @@ -angular.module('portainer.app').component('registriesDatatable', { - templateUrl: './registriesDatatable.html', - controller: 'RegistriesDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - removeAction: '<', - canBrowse: '<', - endpointType: '<', - canManageAccess: '<', - }, -}); diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js b/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js deleted file mode 100644 index da737c8e2..000000000 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js +++ /dev/null @@ -1,91 +0,0 @@ -import { FeatureId } from '@/react/portainer/feature-flags/enums'; -import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; - -angular.module('portainer.docker').controller('RegistriesDatatableController', RegistriesDatatableController); - -/* @ngInject */ -function RegistriesDatatableController($scope, $controller, $state, Authentication, DatatableService) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - - this.allowSelection = function (item) { - return item.Id; - }; - - this.enableGoToLink = (item) => { - return this.isAdmin && item.Id && !this.endpointType; - }; - - this.goToRegistry = function (item) { - if ( - this.endpointType === PortainerEndpointTypes.KubernetesLocalEnvironment || - this.endpointType === PortainerEndpointTypes.AgentOnKubernetesEnvironment || - this.endpointType === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment - ) { - $state.go('kubernetes.registries.registry', { id: item.Id }); - } else if ( - this.endpointType === PortainerEndpointTypes.DockerEnvironment || - this.endpointType === PortainerEndpointTypes.AgentOnDockerEnvironment || - this.endpointType === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment - ) { - $state.go('docker.host.registries.registry', { id: item.Id }); - } else { - $state.go('portainer.registries.registry', { id: item.Id }); - } - }; - - this.redirectToManageAccess = function (item) { - if ( - this.endpointType === PortainerEndpointTypes.KubernetesLocalEnvironment || - this.endpointType === PortainerEndpointTypes.AgentOnKubernetesEnvironment || - this.endpointType === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment - ) { - $state.go('kubernetes.registries.access', { id: item.Id }); - } else { - if (window.location.hash.endsWith('/docker/swarm/registries')) { - $state.go('docker.swarm.registries.access', { id: item.Id }); - } else { - $state.go('docker.host.registries.access', { id: item.Id }); - } - } - }; - - this.$onInit = function () { - this.limitedFeature = FeatureId.REGISTRY_MANAGEMENT; - this.isAdmin = Authentication.isAdmin(); - this.setDefaults(); - this.prepareTableFromDataset(); - - this.state.orderBy = this.orderBy; - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; - } - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - this.onTextFilterChange(); - } - - var storedFilters = DatatableService.getDataTableFilters(this.tableKey); - if (storedFilters !== null) { - this.filters = storedFilters; - } - if (this.filters && this.filters.state) { - this.filters.state.open = false; - } - - var storedSettings = DatatableService.getDataTableSettings(this.tableKey); - if (storedSettings !== null) { - this.settings = storedSettings; - this.settings.open = false; - } - this.onSettingsRepeaterChange(); - - var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey); - if (storedColumnVisibility !== null) { - this.columnVisibility = storedColumnVisibility; - } - }; -} diff --git a/app/portainer/react/components/registries.ts b/app/portainer/react/components/registries.ts index 08fe2bd5c..b096f373a 100644 --- a/app/portainer/react/components/registries.ts +++ b/app/portainer/react/components/registries.ts @@ -2,28 +2,11 @@ import angular from 'angular'; import { r2a } from '@/react-tools/react2angular'; import { withReactQuery } from '@/react-tools/withReactQuery'; -import { - DefaultRegistryAction, - DefaultRegistryDomain, - DefaultRegistryName, -} from '@/react/portainer/registries/ListView/DefaultRegistry'; -import { RepositoriesDatatable } from '@/react/portainer/registries/repositories/ListView/RepositoriesDatatable'; import { withUIRouter } from '@/react-tools/withUIRouter'; +import { RepositoriesDatatable } from '@/react/portainer/registries/repositories/ListView/RepositoriesDatatable'; export const registriesModule = angular .module('portainer.app.react.components.registries', []) - .component( - 'defaultRegistryName', - r2a(withReactQuery(DefaultRegistryName), []) - ) - .component( - 'defaultRegistryAction', - r2a(withReactQuery(DefaultRegistryAction), []) - ) - .component( - 'defaultRegistryDomain', - r2a(withReactQuery(DefaultRegistryDomain), []) - ) .component( 'registryRepositoriesDatatable', r2a(withUIRouter(withReactQuery(RepositoriesDatatable)), ['dataset']) diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts index 6e067d8b4..f3ad06fda 100644 --- a/app/portainer/react/views/index.ts +++ b/app/portainer/react/views/index.ts @@ -17,6 +17,7 @@ import { wizardModule } from './wizard'; import { teamsModule } from './teams'; import { updateSchedulesModule } from './update-schedules'; import { environmentGroupModule } from './env-groups'; +import { registriesModule } from './registries'; export const viewsModule = angular .module('portainer.app.react.views', [ @@ -24,6 +25,7 @@ export const viewsModule = angular teamsModule, updateSchedulesModule, environmentGroupModule, + registriesModule, ]) .component( 'homeView', diff --git a/app/portainer/react/views/registries.ts b/app/portainer/react/views/registries.ts new file mode 100644 index 000000000..7b4cfdd2e --- /dev/null +++ b/app/portainer/react/views/registries.ts @@ -0,0 +1,19 @@ +import angular from 'angular'; + +import { r2a } from '@/react-tools/react2angular'; +import { withCurrentUser } from '@/react-tools/withCurrentUser'; +import { withReactQuery } from '@/react-tools/withReactQuery'; +import { withUIRouter } from '@/react-tools/withUIRouter'; +import { ListView } from '@/react/portainer/registries/ListView'; +import { ListView as EnvironmentListView } from '@/react/portainer/registries/environments/ListView'; + +export const registriesModule = angular + .module('portainer.app.react.views.registries', []) + .component( + 'registriesView', + r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), []) + ) + .component( + 'environmentRegistriesView', + r2a(withUIRouter(withReactQuery(withCurrentUser(EnvironmentListView))), []) + ).name; diff --git a/app/portainer/views/endpoint-registries/registries.html b/app/portainer/views/endpoint-registries/registries.html deleted file mode 100644 index 770dcd5b0..000000000 --- a/app/portainer/views/endpoint-registries/registries.html +++ /dev/null @@ -1,16 +0,0 @@ - - -
-
- -
-
diff --git a/app/portainer/views/endpoint-registries/registries.js b/app/portainer/views/endpoint-registries/registries.js deleted file mode 100644 index 61dfc3b42..000000000 --- a/app/portainer/views/endpoint-registries/registries.js +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('portainer.app').component('endpointRegistriesView', { - templateUrl: './registries.html', - controller: 'EndpointRegistriesController', - bindings: { - endpoint: '<', - }, -}); diff --git a/app/portainer/views/endpoint-registries/registriesController.js b/app/portainer/views/endpoint-registries/registriesController.js deleted file mode 100644 index 72be8f17a..000000000 --- a/app/portainer/views/endpoint-registries/registriesController.js +++ /dev/null @@ -1,54 +0,0 @@ -import _ from 'lodash-es'; -import { RegistryTypes } from 'Portainer/models/registryTypes'; - -class EndpointRegistriesController { - /* @ngInject */ - constructor($async, Notifications, EndpointService, Authentication) { - this.$async = $async; - this.Notifications = Notifications; - this.EndpointService = EndpointService; - this.Authentication = Authentication; - - this.canManageAccess = this.canManageAccess.bind(this); - this.canBrowse = this.canBrowse.bind(this); - } - - canManageAccess(item) { - return item.Type !== RegistryTypes.ANONYMOUS && this.Authentication.isAdmin(); - } - - canBrowse(item) { - return !_.includes([RegistryTypes.ANONYMOUS, RegistryTypes.DOCKERHUB, RegistryTypes.QUAY], item.Type); - } - - getRegistries() { - return this.$async(async () => { - try { - this.registries = await this.EndpointService.registries(this.endpointId); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve registries'); - } - }); - } - - $onInit() { - return this.$async(async () => { - this.state = { - viewReady: false, - }; - - try { - this.endpointType = this.endpoint.Type; - this.endpointId = this.endpoint.Id; - await this.getRegistries(); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve registries'); - } finally { - this.state.viewReady = true; - } - }); - } -} - -export default EndpointRegistriesController; -angular.module('portainer.app').controller('EndpointRegistriesController', EndpointRegistriesController); diff --git a/app/portainer/views/registries/registries.html b/app/portainer/views/registries/registries.html deleted file mode 100644 index f266b5e9d..000000000 --- a/app/portainer/views/registries/registries.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - View registries via an environment to manage access for user(s) and/or team(s) - - -
-
- -
-
diff --git a/app/portainer/views/registries/registriesController.js b/app/portainer/views/registries/registriesController.js deleted file mode 100644 index 046c463b6..000000000 --- a/app/portainer/views/registries/registriesController.js +++ /dev/null @@ -1,71 +0,0 @@ -import _ from 'lodash-es'; -import { confirmDelete } from '@@/modals/confirm'; -import { RegistryTypes } from 'Portainer/models/registryTypes'; - -angular.module('portainer.app').controller('RegistriesController', [ - '$q', - '$scope', - '$state', - 'RegistryService', - 'Notifications', - function ($q, $scope, $state, RegistryService, Notifications) { - $scope.state = { - actionInProgress: false, - }; - - const nonBrowsableTypes = [RegistryTypes.ANONYMOUS, RegistryTypes.DOCKERHUB, RegistryTypes.QUAY]; - - $scope.canBrowse = function (item) { - return !_.includes(nonBrowsableTypes, item.Type); - }; - - $scope.removeAction = function (selectedItems) { - const regAttrMsg = selectedItems.length > 1 ? 'hese' : 'his'; - const registriesMsg = selectedItems.length > 1 ? 'registries' : 'registry'; - const msg = `T${regAttrMsg} ${registriesMsg} might be used by applications inside one or more environments. Removing the ${registriesMsg} could lead to a service interruption for the applications using t${regAttrMsg} ${registriesMsg}. Do you want to remove the selected ${registriesMsg}?`; - - confirmDelete(msg).then((confirmed) => { - if (!confirmed) { - return; - } - deleteSelectedRegistries(selectedItems); - }); - }; - - function deleteSelectedRegistries(selectedItems) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (registry) { - RegistryService.deleteRegistry(registry.Id) - .then(function success() { - Notifications.success('Registry successfully removed', registry.Name); - var index = $scope.registries.indexOf(registry); - $scope.registries.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove registry'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } - }); - }); - } - - function initView() { - $q.all({ - registries: RegistryService.registries(), - }) - .then(function success(data) { - $scope.registries = data.registries; - }) - .catch(function error(err) { - $scope.registries = []; - Notifications.error('Failure', err, 'Unable to retrieve registries'); - }); - } - - initView(); - }, -]); diff --git a/app/react/components/BEFeatureIndicator/BEFeatureIndicator.tsx b/app/react/components/BEFeatureIndicator/BEFeatureIndicator.tsx index ebccf3bce..47ff2e787 100644 --- a/app/react/components/BEFeatureIndicator/BEFeatureIndicator.tsx +++ b/app/react/components/BEFeatureIndicator/BEFeatureIndicator.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from 'react'; +import { ReactNode } from 'react'; import clsx from 'clsx'; import { Briefcase } from 'lucide-react'; @@ -11,32 +11,39 @@ import { Icon } from '@@/Icon'; import { getFeatureDetails } from './utils'; export interface Props { - featureId?: FeatureId; + featureId: FeatureId; showIcon?: boolean; className?: string; + children?: (isLimited: boolean) => ReactNode; } export function BEFeatureIndicator({ featureId, - children, + children = () => null, showIcon = true, className = '', -}: PropsWithChildren) { - const { url, limitedToBE } = getFeatureDetails(featureId); +}: Props) { + const { url, limitedToBE = false } = getFeatureDetails(featureId); - if (!limitedToBE) { - return null; - } return ( - - {children} - {showIcon && } - Business Feature - + <> + {limitedToBE && ( + + {showIcon && ( + + )} + + Business Feature + + + )} + + {children(limitedToBE)} + ); } diff --git a/app/react/components/form-components/SwitchField/Switch.tsx b/app/react/components/form-components/SwitchField/Switch.tsx index 2ad968fae..1a6ae26a2 100644 --- a/app/react/components/form-components/SwitchField/Switch.tsx +++ b/app/react/components/form-components/SwitchField/Switch.tsx @@ -55,7 +55,7 @@ export function Switch({ /> - {limitedToBE && } + {featureId && limitedToBE && } ); } diff --git a/app/react/docker/images/ItemView/RegistrySelectPrompt.tsx b/app/react/docker/images/ItemView/RegistrySelectPrompt.tsx index 024af8454..de1c8541d 100644 --- a/app/react/docker/images/ItemView/RegistrySelectPrompt.tsx +++ b/app/react/docker/images/ItemView/RegistrySelectPrompt.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; import { Modal, OnSubmit, openModal } from '@@/modals'; import { Button } from '@@/buttons'; diff --git a/app/react/docker/services/webhooks/types.ts b/app/react/docker/services/webhooks/types.ts index 5eef4e071..4f5991b06 100644 --- a/app/react/docker/services/webhooks/types.ts +++ b/app/react/docker/services/webhooks/types.ts @@ -1,5 +1,5 @@ import { Environment } from '@/react/portainer/environments/types'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; enum WebhookType { Service = 1, diff --git a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx index 04a43235b..76d6b2406 100644 --- a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx +++ b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx @@ -29,7 +29,7 @@ import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGrou import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector'; import { notifySuccess } from '@/portainer/services/notifications'; import { EnvironmentType } from '@/react/portainer/environments/types'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; import { useRegistries } from '@/react/portainer/registries/queries/useRegistries'; import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset'; import { parseRelativePathResponse } from '@/react/portainer/gitops/RelativePathFieldset/utils'; diff --git a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/useUpdateEdgeStackGitMutation.ts b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/useUpdateEdgeStackGitMutation.ts index 2d1ef2b46..aea5ccf74 100644 --- a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/useUpdateEdgeStackGitMutation.ts +++ b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/useUpdateEdgeStackGitMutation.ts @@ -9,7 +9,7 @@ import { import { buildUrl } from '@/react/edge/edge-stacks/queries/buildUrl'; import { DeploymentType, EdgeStack } from '@/react/edge/edge-stacks/types'; import { EdgeGroup } from '@/react/edge/edge-groups/types'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; export interface UpdateEdgeStackGitPayload { id: EdgeStack['Id']; diff --git a/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tsx b/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tsx index b267c8bd0..d125a25ac 100644 --- a/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tsx +++ b/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { RefreshCw } from 'lucide-react'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; import { Select } from '@@/form-components/ReactSelect'; import { FormControl } from '@@/form-components/FormControl'; diff --git a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromFile.ts b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromFile.ts index 089f8fe89..5542f02f3 100644 --- a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromFile.ts +++ b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromFile.ts @@ -2,7 +2,7 @@ import axios, { json2formData, parseAxiosError, } from '@/portainer/services/axios'; -import { RegistryId } from '@/react/portainer/registries/types'; +import { RegistryId } from '@/react/portainer/registries/types/registry'; import { Pair } from '@/react/portainer/settings/types'; import { EdgeGroup } from '@/react/edge/edge-groups/types'; diff --git a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromFileContent.ts b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromFileContent.ts index 299968bad..9266275d7 100644 --- a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromFileContent.ts +++ b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromFileContent.ts @@ -1,5 +1,5 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { RegistryId } from '@/react/portainer/registries/types'; +import { RegistryId } from '@/react/portainer/registries/types/registry'; import { Pair } from '@/react/portainer/settings/types'; import { EdgeGroup } from '@/react/edge/edge-groups/types'; diff --git a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromGit.ts b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromGit.ts index e7a9cb6a9..81277d90b 100644 --- a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromGit.ts +++ b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromGit.ts @@ -1,5 +1,5 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { RegistryId } from '@/react/portainer/registries/types'; +import { RegistryId } from '@/react/portainer/registries/types/registry'; import { Pair } from '@/react/portainer/settings/types'; import { EdgeGroup } from '@/react/edge/edge-groups/types'; import { AutoUpdateModel } from '@/react/portainer/gitops/types'; diff --git a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/useCreateEdgeStack.ts b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/useCreateEdgeStack.ts index 80cc1350e..159797986 100644 --- a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/useCreateEdgeStack.ts +++ b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/useCreateEdgeStack.ts @@ -1,7 +1,7 @@ import { useMutation } from 'react-query'; import { EdgeGroup } from '@/react/edge/edge-groups/types'; -import { RegistryId } from '@/react/portainer/registries/types'; +import { RegistryId } from '@/react/portainer/registries/types/registry'; import { Pair } from '@/react/portainer/settings/types'; import { GitFormModel, diff --git a/app/react/edge/edge-stacks/queries/useParseRegistries.ts b/app/react/edge/edge-stacks/queries/useParseRegistries.ts index e13f2334e..3d334453d 100644 --- a/app/react/edge/edge-stacks/queries/useParseRegistries.ts +++ b/app/react/edge/edge-stacks/queries/useParseRegistries.ts @@ -1,7 +1,7 @@ import { useMutation } from 'react-query'; import { withError } from '@/react-tools/react-query'; -import { RegistryId } from '@/react/portainer/registries/types'; +import { RegistryId } from '@/react/portainer/registries/types/registry'; import axios, { json2formData, parseAxiosError, diff --git a/app/react/edge/edge-stacks/types.ts b/app/react/edge/edge-stacks/types.ts index 8e6f7e30e..d03666b09 100644 --- a/app/react/edge/edge-stacks/types.ts +++ b/app/react/edge/edge-stacks/types.ts @@ -4,7 +4,7 @@ import { RelativePathModel, RepoConfigResponse, } from '@/react/portainer/gitops/types'; -import { RegistryId } from '@/react/portainer/registries/types'; +import { RegistryId } from '@/react/portainer/registries/types/registry'; import { EnvVar } from '@@/form-components/EnvironmentVariablesFieldset/types'; diff --git a/app/react/kubernetes/namespaces/CreateView/types.ts b/app/react/kubernetes/namespaces/CreateView/types.ts index 5a8cd0f5d..9aa04b20c 100644 --- a/app/react/kubernetes/namespaces/CreateView/types.ts +++ b/app/react/kubernetes/namespaces/CreateView/types.ts @@ -1,4 +1,4 @@ -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; import { IngressControllerClassMap } from '../../cluster/ingressClass/types'; import { diff --git a/app/react/kubernetes/namespaces/components/NamespaceInnerForm.tsx b/app/react/kubernetes/namespaces/components/NamespaceInnerForm.tsx index 3184fd967..878117403 100644 --- a/app/react/kubernetes/namespaces/components/NamespaceInnerForm.tsx +++ b/app/react/kubernetes/namespaces/components/NamespaceInnerForm.tsx @@ -3,7 +3,7 @@ import { MultiValue } from 'react-select'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; import { FormControl } from '@@/form-components/FormControl'; import { FormSection } from '@@/form-components/FormSection'; diff --git a/app/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesFormSection.tsx b/app/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesFormSection.tsx index 3cd8a2822..597a4b86d 100644 --- a/app/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesFormSection.tsx +++ b/app/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesFormSection.tsx @@ -1,7 +1,7 @@ import { FormikErrors } from 'formik'; import { MultiValue } from 'react-select'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; diff --git a/app/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector.tsx b/app/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector.tsx index d5b519d80..0d270afc2 100644 --- a/app/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector.tsx +++ b/app/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector.tsx @@ -1,6 +1,6 @@ import { MultiValue } from 'react-select'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; import { useCurrentUser } from '@/react/hooks/useUser'; import { Select } from '@@/form-components/ReactSelect'; diff --git a/app/react/kubernetes/namespaces/components/RegistriesFormSection/registriesValidationSchema.ts b/app/react/kubernetes/namespaces/components/RegistriesFormSection/registriesValidationSchema.ts index 5206187f3..51f5bb736 100644 --- a/app/react/kubernetes/namespaces/components/RegistriesFormSection/registriesValidationSchema.ts +++ b/app/react/kubernetes/namespaces/components/RegistriesFormSection/registriesValidationSchema.ts @@ -1,10 +1,11 @@ import { SchemaOf, array, object, number, string } from 'yup'; -import { Registry } from '@/react/portainer/registries/types'; +import { Registry } from '@/react/portainer/registries/types/registry'; export const registriesValidationSchema: SchemaOf = array( object({ Id: number().required('Registry ID is required.'), Name: string().required('Registry name is required.'), - }) + }) as unknown as SchemaOf + // the only needed value is actually the id. SchemaOf throw a ts error if we don't cast to SchemaOf ); diff --git a/app/react/portainer/environments/environment.service/registries.ts b/app/react/portainer/environments/environment.service/registries.ts index 875b31ad6..024fc1877 100644 --- a/app/react/portainer/environments/environment.service/registries.ts +++ b/app/react/portainer/environments/environment.service/registries.ts @@ -1,7 +1,10 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; import { TeamId } from '@/react/portainer/users/teams/types'; import { UserId } from '@/portainer/users/types'; -import { RegistryId, Registry } from '@/react/portainer/registries/types'; +import { + RegistryId, + Registry, +} from '@/react/portainer/registries/types/registry'; import { EnvironmentId } from '../types'; diff --git a/app/react/portainer/environments/wizard/components/Option/Option.tsx b/app/react/portainer/environments/wizard/components/Option/Option.tsx index e3bd66aa4..9626953ef 100644 --- a/app/react/portainer/environments/wizard/components/Option/Option.tsx +++ b/app/react/portainer/environments/wizard/components/Option/Option.tsx @@ -51,7 +51,7 @@ export function Option({

{title}

{description}
- {isLimited && ( + {featureId && isLimited && ( + + + + + View registries via an environment to manage access for user(s) and/or + team(s) + + + + + + ); +} diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/AddButton.tsx b/app/react/portainer/registries/ListView/RegistriesDatatable/AddButton.tsx new file mode 100644 index 000000000..ab53a29e3 --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/AddButton.tsx @@ -0,0 +1,12 @@ +import { AddButton as BaseAddButton } from '@@/buttons'; + +export function AddButton() { + return ( + + Add registry + + ); +} diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/DeleteButton.tsx b/app/react/portainer/registries/ListView/RegistriesDatatable/DeleteButton.tsx new file mode 100644 index 000000000..8913813cb --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/DeleteButton.tsx @@ -0,0 +1,40 @@ +import { pluralize } from '@/portainer/helpers/strings'; +import { notifySuccess } from '@/portainer/services/notifications'; + +import { DeleteButton as BaseDeleteButton } from '@@/buttons/DeleteButton'; + +import { Registry } from '../../types/registry'; + +import { useDeleteRegistriesMutation } from './useDeleteRegistriesMutation'; + +export function DeleteButton({ selectedItems }: { selectedItems: Registry[] }) { + const mutation = useDeleteRegistriesMutation(); + + const confirmMessage = getMessage(selectedItems.length); + + return ( + + ); + + function handleDelete() { + mutation.mutate( + selectedItems.map((item) => item.Id), + { + onSuccess() { + notifySuccess('Success', 'Registries removed'); + }, + } + ); + } +} + +function getMessage(selectedCount: number) { + const regAttrMsg = selectedCount > 1 ? 'hese' : 'his'; + const registriesMsg = pluralize(selectedCount, 'registry', 'registries'); + return `T${regAttrMsg} ${registriesMsg} might be used by applications inside one or more environments. Removing the ${registriesMsg} could lead to a service interruption for the applications using t${regAttrMsg} ${registriesMsg}. Do you want to remove the selected ${registriesMsg}?`; +} diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/RegistriesDatatable.tsx b/app/react/portainer/registries/ListView/RegistriesDatatable/RegistriesDatatable.tsx new file mode 100644 index 000000000..e9b8d223e --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/RegistriesDatatable.tsx @@ -0,0 +1,40 @@ +import { Radio } from 'lucide-react'; + +import { Datatable } from '@@/datatables'; +import { createPersistedStore } from '@@/datatables/types'; +import { useTableState } from '@@/datatables/useTableState'; + +import { useRegistries } from '../../queries/useRegistries'; + +import { columns } from './columns'; +import { DeleteButton } from './DeleteButton'; +import { AddButton } from './AddButton'; + +const tableKey = 'registries'; + +const store = createPersistedStore(tableKey); + +export function RegistriesDatatable() { + const query = useRegistries(); + + const tableState = useTableState(store, tableKey); + + return ( + ( + <> + + + + + )} + isRowSelectable={(row) => !!row.original.Id} + /> + ); +} diff --git a/app/react/portainer/registries/ListView/DefaultRegistry/DefaultRegistryAction.tsx b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/DefaultRegistryAction.tsx similarity index 98% rename from app/react/portainer/registries/ListView/DefaultRegistry/DefaultRegistryAction.tsx rename to app/react/portainer/registries/ListView/RegistriesDatatable/columns/DefaultRegistryAction.tsx index f2209d0c5..d74ce2f6a 100644 --- a/app/react/portainer/registries/ListView/DefaultRegistry/DefaultRegistryAction.tsx +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/DefaultRegistryAction.tsx @@ -38,7 +38,7 @@ export function DefaultRegistryAction() { Hide for all users - {isLimited ? null : ( + {isLimited && ( ) { + if (!item.Id) { + return ; + } + + return ; +} + +export function BrowseButton({ + registryId, + registryType, + environmentId, +}: { + registryId: RegistryId; + registryType: RegistryTypes; + environmentId?: EnvironmentId; +}) { + const canBrowse = !nonBrowsableTypes.includes(registryType); + + if (!canBrowse) { + return null; + } + + return ( + + {(isLimited) => ( + + )} + + ); +} diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/columns/helper.ts b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/helper.ts new file mode 100644 index 000000000..705dd56e7 --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/helper.ts @@ -0,0 +1,5 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { DecoratedRegistry } from '../types'; + +export const columnHelper = createColumnHelper(); diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/columns/index.ts b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/index.ts new file mode 100644 index 000000000..e91063831 --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/index.ts @@ -0,0 +1,5 @@ +import { actions } from './actions'; +import { name } from './name'; +import { url } from './url'; + +export const columns = [name, url, actions]; diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/columns/name.tsx b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/name.tsx new file mode 100644 index 000000000..7c0a5f66d --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/name.tsx @@ -0,0 +1,52 @@ +import { CellContext } from '@tanstack/react-table'; + +import { useIsEdgeAdmin } from '@/react/hooks/useUser'; + +import { Link } from '@@/Link'; + +import { DecoratedRegistry } from '../types'; + +import { columnHelper } from './helper'; +import { DefaultRegistryName } from './DefaultRegistryName'; + +export const name = columnHelper.accessor('Name', { + header: 'Name', + cell: Cell, +}); + +function Cell({ + row: { original: item }, +}: CellContext) { + return ; +} + +export function NameCell({ + item, + hasLink, +}: { + item: DecoratedRegistry; + hasLink?: boolean; +}) { + const isEdgeAdminQuery = useIsEdgeAdmin(); + + if (!item.Id) { + return ; + } + + return ( + <> + {isEdgeAdminQuery.isAdmin && hasLink ? ( + + {item.Name} + + ) : ( + item.Name + )} + {item.Authentication && ( + + authentication-enabled + + )} + + ); +} diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/columns/url.tsx b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/url.tsx new file mode 100644 index 000000000..e09731d4e --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/columns/url.tsx @@ -0,0 +1,8 @@ +import { DefaultRegistryDomain } from './DefaultRegistryDomain'; +import { columnHelper } from './helper'; + +export const url = columnHelper.accessor('URL', { + header: 'URL', + cell: ({ getValue, row: { original: item } }) => + item.Id ? getValue() : , +}); diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/index.ts b/app/react/portainer/registries/ListView/RegistriesDatatable/index.ts new file mode 100644 index 000000000..a811803ef --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/index.ts @@ -0,0 +1 @@ +export { RegistriesDatatable } from './RegistriesDatatable'; diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/types.ts b/app/react/portainer/registries/ListView/RegistriesDatatable/types.ts new file mode 100644 index 000000000..ab234944b --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/types.ts @@ -0,0 +1,3 @@ +import { Registry } from '../../types/registry'; + +export interface DecoratedRegistry extends Registry {} diff --git a/app/react/portainer/registries/ListView/RegistriesDatatable/useDeleteRegistriesMutation.ts b/app/react/portainer/registries/ListView/RegistriesDatatable/useDeleteRegistriesMutation.ts new file mode 100644 index 000000000..fe4a281fe --- /dev/null +++ b/app/react/portainer/registries/ListView/RegistriesDatatable/useDeleteRegistriesMutation.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from 'react-query'; + +import { promiseSequence } from '@/portainer/helpers/promise-utils'; +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { + mutationOptions, + withError, + withInvalidate, +} from '@/react-tools/react-query'; + +import { buildUrl } from '../../queries/build-url'; +import { queryKeys } from '../../queries/query-keys'; +import { Registry } from '../../types/registry'; + +export function useDeleteRegistriesMutation() { + const queryClient = useQueryClient(); + return useMutation( + (RegistryIds: Array) => + promiseSequence( + RegistryIds.map((RegistryId) => () => deleteRegistry(RegistryId)) + ), + mutationOptions( + withError('Unable to delete registries'), + withInvalidate(queryClient, [queryKeys.base()]) + ) + ); +} + +async function deleteRegistry(id: Registry['Id']) { + try { + await axios.delete(buildUrl(id)); + } catch (e) { + throw parseAxiosError(e, 'Unable to delete registries'); + } +} diff --git a/app/react/portainer/registries/ListView/index.ts b/app/react/portainer/registries/ListView/index.ts new file mode 100644 index 000000000..dd06dfd19 --- /dev/null +++ b/app/react/portainer/registries/ListView/index.ts @@ -0,0 +1 @@ +export { ListView } from './ListView'; diff --git a/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/EnvironmentRegistriesDatatable.tsx b/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/EnvironmentRegistriesDatatable.tsx new file mode 100644 index 000000000..60cd7c54d --- /dev/null +++ b/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/EnvironmentRegistriesDatatable.tsx @@ -0,0 +1,39 @@ +import { Radio } from 'lucide-react'; + +import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries'; +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; +import { url } from '@/react/portainer/registries/ListView/RegistriesDatatable/columns/url'; +import { AddButton } from '@/react/portainer/registries/ListView/RegistriesDatatable/AddButton'; + +import { Datatable } from '@@/datatables'; +import { createPersistedStore } from '@@/datatables/types'; +import { useTableState } from '@@/datatables/useTableState'; + +import { name } from './columns/name'; +import { actions } from './columns/actions'; + +const columns = [name, url, actions]; + +const tableKey = 'registries'; + +const store = createPersistedStore(tableKey); + +export function EnvironmentRegistriesDatatable() { + const environmentId = useEnvironmentId(); + const query = useEnvironmentRegistries(environmentId); + + const tableState = useTableState(store, tableKey); + + return ( + } + disableSelect + /> + ); +} diff --git a/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/columns/actions.tsx b/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/columns/actions.tsx new file mode 100644 index 000000000..e65a4a4d7 --- /dev/null +++ b/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/columns/actions.tsx @@ -0,0 +1,56 @@ +import { CellContext } from '@tanstack/react-table'; +import { Users } from 'lucide-react'; + +import { Authorized, useAuthorizations } from '@/react/hooks/useUser'; +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; +import { DecoratedRegistry } from '@/react/portainer/registries/ListView/RegistriesDatatable/types'; +import { RegistryTypes } from '@/react/portainer/registries/types/registry'; +import { columnHelper } from '@/react/portainer/registries/ListView/RegistriesDatatable/columns/helper'; +import { BrowseButton } from '@/react/portainer/registries/ListView/RegistriesDatatable/columns/actions'; + +import { Button } from '@@/buttons'; +import { Link } from '@@/Link'; + +export const actions = columnHelper.display({ + header: 'Actions', + cell: Cell, +}); + +function Cell({ + row: { original: item }, +}: CellContext) { + const environmentId = useEnvironmentId(); + const hasUpdateAccessAuthorizations = useAuthorizations( + ['PortainerRegistryUpdateAccess'], + environmentId, + true + ); + const canManageAccess = + item.Type !== RegistryTypes.ANONYMOUS && hasUpdateAccessAuthorizations; + + if (!item.Id) { + return null; + } + + return ( + <> + {canManageAccess && ( + + + + )} + + + ); +} diff --git a/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/columns/name.tsx b/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/columns/name.tsx new file mode 100644 index 000000000..74c0a9888 --- /dev/null +++ b/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/columns/name.tsx @@ -0,0 +1,16 @@ +import { CellContext } from '@tanstack/react-table'; + +import { DecoratedRegistry } from '@/react/portainer/registries/ListView/RegistriesDatatable/types'; +import { columnHelper } from '@/react/portainer/registries/ListView/RegistriesDatatable/columns/helper'; +import { NameCell } from '@/react/portainer/registries/ListView/RegistriesDatatable/columns/name'; + +export const name = columnHelper.accessor('Name', { + header: 'Name', + cell: Cell, +}); + +function Cell({ + row: { original: item }, +}: CellContext) { + return ; +} diff --git a/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/index.ts b/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/index.ts new file mode 100644 index 000000000..1fdecc69a --- /dev/null +++ b/app/react/portainer/registries/environments/ListView/EnvironmentRegistriesDatatable/index.ts @@ -0,0 +1 @@ +export { EnvironmentRegistriesDatatable } from './EnvironmentRegistriesDatatable'; diff --git a/app/react/portainer/registries/environments/ListView/ListView.tsx b/app/react/portainer/registries/environments/ListView/ListView.tsx new file mode 100644 index 000000000..1847fb1e4 --- /dev/null +++ b/app/react/portainer/registries/environments/ListView/ListView.tsx @@ -0,0 +1,17 @@ +import { PageHeader } from '@@/PageHeader'; + +import { EnvironmentRegistriesDatatable } from './EnvironmentRegistriesDatatable'; + +export function ListView() { + return ( + <> + + + + + ); +} diff --git a/app/react/portainer/registries/environments/ListView/index.ts b/app/react/portainer/registries/environments/ListView/index.ts new file mode 100644 index 000000000..dd06dfd19 --- /dev/null +++ b/app/react/portainer/registries/environments/ListView/index.ts @@ -0,0 +1 @@ +export { ListView } from './ListView'; diff --git a/app/react/portainer/registries/queries/query-keys.ts b/app/react/portainer/registries/queries/query-keys.ts index 5afa6c0fc..625ca0504 100644 --- a/app/react/portainer/registries/queries/query-keys.ts +++ b/app/react/portainer/registries/queries/query-keys.ts @@ -1,6 +1,9 @@ +import { EnvironmentId } from '../../environments/types'; import { RegistryId } from '../types/registry'; export const queryKeys = { base: () => ['registries'] as const, + list: (environmentId?: EnvironmentId) => + [...queryKeys.base(), { environmentId }] as const, item: (registryId: RegistryId) => [...queryKeys.base(), registryId] as const, }; diff --git a/app/react/portainer/registries/types.ts b/app/react/portainer/registries/types.ts deleted file mode 100644 index 63ffbbcdb..000000000 --- a/app/react/portainer/registries/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type RegistryId = number; -export interface Registry { - Id: RegistryId; - Name: string; -} diff --git a/app/react/portainer/templates/custom-templates/types.ts b/app/react/portainer/templates/custom-templates/types.ts index 0b6230860..d1b2c9462 100644 --- a/app/react/portainer/templates/custom-templates/types.ts +++ b/app/react/portainer/templates/custom-templates/types.ts @@ -5,7 +5,7 @@ import { ResourceControlResponse } from '../../access-control/types'; import { RelativePathModel, RepoConfigResponse } from '../../gitops/types'; import { VariableDefinition } from '../../custom-templates/components/CustomTemplatesVariablesDefinitionField'; import { Platform } from '../types'; -import { RegistryId } from '../../registries/types'; +import { RegistryId } from '../../registries/types/registry'; import { getDefaultRelativePathModel } from '../../gitops/RelativePathFieldset/types'; import { isBE } from '../../feature-flags/feature-flags.service';