mirror of https://github.com/portainer/portainer
refactor(registries): migrate list view to react [EE-4704] (#10687)
parent
9600eb6fa1
commit
f584bf3830
|
@ -603,7 +603,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||||
url: '/registries',
|
url: '/registries',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'endpointRegistriesView',
|
component: 'environmentRegistriesView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
@ -616,7 +616,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||||
url: '/registries',
|
url: '/registries',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'endpointRegistriesView',
|
component: 'environmentRegistriesView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -518,7 +518,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
url: '/registries',
|
url: '/registries',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'endpointRegistriesView',
|
component: 'environmentRegistriesView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -356,8 +356,7 @@ angular
|
||||||
url: '/registries',
|
url: '/registries',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: './views/registries/registries.html',
|
component: 'registriesView',
|
||||||
controller: 'RegistriesController',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
<div class="datatable">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="toolBar">
|
|
||||||
<div class="toolBarTitle vertical-center">
|
|
||||||
<div class="widget-icon space-right">
|
|
||||||
<pr-icon icon="$ctrl.titleIcon"></pr-icon>
|
|
||||||
</div>
|
|
||||||
{{ $ctrl.titleText }}
|
|
||||||
</div>
|
|
||||||
<div class="searchBar vertical-center">
|
|
||||||
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="searchInput"
|
|
||||||
ng-model="$ctrl.state.textFilter"
|
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
|
||||||
placeholder="Search for a registry..."
|
|
||||||
auto-focus
|
|
||||||
ng-model-options="{ debounce: 300 }"
|
|
||||||
data-cy="registry-searchInput"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="actionBar !gap-3" ng-if="$ctrl.isAdmin">
|
|
||||||
<button
|
|
||||||
ng-if="!$ctrl.endpointType"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
|
||||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
|
||||||
data-cy="registry-removeRegistryButton"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'trash-2'"></pr-icon>Remove
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-primary vertical-center !ml-0 h-fit" ui-sref="portainer.registries.new" data-cy="registry-addRegistryButton">
|
|
||||||
<pr-icon icon="'plus'"></pr-icon>Add registry
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table-hover nowrap-cells table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<div class="vertical-center">
|
|
||||||
<span class="md-checkbox vertical-center" ng-if="$ctrl.isAdmin && !$ctrl.endpointType">
|
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
|
||||||
<label for="select_all"></label>
|
|
||||||
</span>
|
|
||||||
<table-column-header
|
|
||||||
col-title="'Name'"
|
|
||||||
can-sort="true"
|
|
||||||
is-sorted="$ctrl.state.orderBy === 'Name'"
|
|
||||||
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
|
|
||||||
ng-click="$ctrl.changeOrderBy('Name')"
|
|
||||||
></table-column-header>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<table-column-header
|
|
||||||
col-title="'URL'"
|
|
||||||
can-sort="true"
|
|
||||||
is-sorted="$ctrl.state.orderBy === 'URL'"
|
|
||||||
is-sorted-desc="$ctrl.state.orderBy === 'URL' && $ctrl.state.reverseOrder"
|
|
||||||
ng-click="$ctrl.changeOrderBy('URL')"
|
|
||||||
></table-column-header>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<table-column-header col-title="'Actions'" can-sort="false"></table-column-header>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<span class="md-checkbox" ng-if="$ctrl.isAdmin && !$ctrl.endpointType">
|
|
||||||
<input id="select_{{ $index }}" type="checkbox" disabled />
|
|
||||||
<label for="select_{{ $index }}"></label>
|
|
||||||
</span>
|
|
||||||
<span><default-registry-name></default-registry-name></span>
|
|
||||||
</td>
|
|
||||||
<td> <default-registry-domain></default-registry-domain> </td>
|
|
||||||
<td>
|
|
||||||
<default-registry-action ng-if="$ctrl.isAdmin && !$ctrl.endpointType"></default-registry-action>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
|
|
||||||
ng-class="{ active: item.Checked }"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<span class="md-checkbox" ng-if="$ctrl.isAdmin && !$ctrl.endpointType">
|
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
|
|
||||||
<label for="select_{{ $index }}"></label>
|
|
||||||
</span>
|
|
||||||
<a ui-sref="portainer.registries.registry({id: item.Id})" ng-if="$ctrl.enableGoToLink(item)">{{ item.Name }}</a>
|
|
||||||
<span ng-if="!$ctrl.enableGoToLink(item)">{{ item.Name }}</span>
|
|
||||||
<span ng-if="item.Authentication" style="margin-left: 5px" class="label label-info image-tag">authentication-enabled</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ item.URL }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-link vertical-center !ml-0 px-0 hover:no-underline"
|
|
||||||
ng-if="$ctrl.canManageAccess(item)"
|
|
||||||
ng-click="$ctrl.redirectToManageAccess(item)"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'users'"></pr-icon>Manage access
|
|
||||||
</button>
|
|
||||||
<be-feature-indicator feature="$ctrl.limitedFeature" ng-if="$ctrl.canBrowse(item)">
|
|
||||||
<span class="text-muted" style="padding-right: 5px"> <pr-icon icon="'search'"></pr-icon> Browse </span>
|
|
||||||
</be-feature-indicator>
|
|
||||||
|
|
||||||
<span ng-if="!$ctrl.canBrowse(item) && !$ctrl.canManageAccess(item)"> - </span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="!$ctrl.dataset">
|
|
||||||
<td colspan="3" class="text-muted text-center">Loading...</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="footer" ng-if="$ctrl.dataset">
|
|
||||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
|
||||||
<div class="paginationControls">
|
|
||||||
<form class="form-inline vertical-center">
|
|
||||||
<span class="limitSelector">
|
|
||||||
<span style="margin-right: 5px"> Items per page </span>
|
|
||||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
|
|
||||||
<option value="0">All</option>
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
|
@ -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: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -2,28 +2,11 @@ import angular from 'angular';
|
||||||
|
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
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 { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
|
import { RepositoriesDatatable } from '@/react/portainer/registries/repositories/ListView/RepositoriesDatatable';
|
||||||
|
|
||||||
export const registriesModule = angular
|
export const registriesModule = angular
|
||||||
.module('portainer.app.react.components.registries', [])
|
.module('portainer.app.react.components.registries', [])
|
||||||
.component(
|
|
||||||
'defaultRegistryName',
|
|
||||||
r2a(withReactQuery(DefaultRegistryName), [])
|
|
||||||
)
|
|
||||||
.component(
|
|
||||||
'defaultRegistryAction',
|
|
||||||
r2a(withReactQuery(DefaultRegistryAction), [])
|
|
||||||
)
|
|
||||||
.component(
|
|
||||||
'defaultRegistryDomain',
|
|
||||||
r2a(withReactQuery(DefaultRegistryDomain), [])
|
|
||||||
)
|
|
||||||
.component(
|
.component(
|
||||||
'registryRepositoriesDatatable',
|
'registryRepositoriesDatatable',
|
||||||
r2a(withUIRouter(withReactQuery(RepositoriesDatatable)), ['dataset'])
|
r2a(withUIRouter(withReactQuery(RepositoriesDatatable)), ['dataset'])
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { wizardModule } from './wizard';
|
||||||
import { teamsModule } from './teams';
|
import { teamsModule } from './teams';
|
||||||
import { updateSchedulesModule } from './update-schedules';
|
import { updateSchedulesModule } from './update-schedules';
|
||||||
import { environmentGroupModule } from './env-groups';
|
import { environmentGroupModule } from './env-groups';
|
||||||
|
import { registriesModule } from './registries';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.app.react.views', [
|
.module('portainer.app.react.views', [
|
||||||
|
@ -24,6 +25,7 @@ export const viewsModule = angular
|
||||||
teamsModule,
|
teamsModule,
|
||||||
updateSchedulesModule,
|
updateSchedulesModule,
|
||||||
environmentGroupModule,
|
environmentGroupModule,
|
||||||
|
registriesModule,
|
||||||
])
|
])
|
||||||
.component(
|
.component(
|
||||||
'homeView',
|
'homeView',
|
||||||
|
|
|
@ -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;
|
|
@ -1,16 +0,0 @@
|
||||||
<page-header title="'Environment registries'" breadcrumbs="['Registry management']" reload="true"> </page-header>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<registries-datatable
|
|
||||||
title-text="Registries"
|
|
||||||
title-icon="radio"
|
|
||||||
dataset="$ctrl.registries"
|
|
||||||
table-key="endpointRegistries"
|
|
||||||
order-by="Name"
|
|
||||||
endpoint-type="$ctrl.endpointType"
|
|
||||||
can-manage-access="$ctrl.canManageAccess"
|
|
||||||
can-browse="$ctrl.canBrowse"
|
|
||||||
></registries-datatable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,7 +0,0 @@
|
||||||
angular.module('portainer.app').component('endpointRegistriesView', {
|
|
||||||
templateUrl: './registries.html',
|
|
||||||
controller: 'EndpointRegistriesController',
|
|
||||||
bindings: {
|
|
||||||
endpoint: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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);
|
|
|
@ -1,19 +0,0 @@
|
||||||
<page-header title="'Registries'" breadcrumbs="['Registry management']" reload="true"> </page-header>
|
|
||||||
|
|
||||||
<information-panel title-text="Information">
|
|
||||||
<span class="small text-muted"> View registries via an environment to manage access for user(s) and/or team(s) </span>
|
|
||||||
</information-panel>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<registries-datatable
|
|
||||||
title-text="Registries"
|
|
||||||
title-icon="radio"
|
|
||||||
dataset="registries"
|
|
||||||
table-key="registries"
|
|
||||||
order-by="Name"
|
|
||||||
remove-action="removeAction"
|
|
||||||
can-browse="canBrowse"
|
|
||||||
></registries-datatable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -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();
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Briefcase } from 'lucide-react';
|
import { Briefcase } from 'lucide-react';
|
||||||
|
|
||||||
|
@ -11,32 +11,39 @@ import { Icon } from '@@/Icon';
|
||||||
import { getFeatureDetails } from './utils';
|
import { getFeatureDetails } from './utils';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
featureId?: FeatureId;
|
featureId: FeatureId;
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
children?: (isLimited: boolean) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BEFeatureIndicator({
|
export function BEFeatureIndicator({
|
||||||
featureId,
|
featureId,
|
||||||
children,
|
children = () => null,
|
||||||
showIcon = true,
|
showIcon = true,
|
||||||
className = '',
|
className = '',
|
||||||
}: PropsWithChildren<Props>) {
|
}: Props) {
|
||||||
const { url, limitedToBE } = getFeatureDetails(featureId);
|
const { url, limitedToBE = false } = getFeatureDetails(featureId);
|
||||||
|
|
||||||
if (!limitedToBE) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<a
|
<>
|
||||||
className={clsx('be-indicator vertical-center text-xs', className)}
|
{limitedToBE && (
|
||||||
href={url}
|
<a
|
||||||
target="_blank"
|
className={clsx('be-indicator vertical-center text-xs', className)}
|
||||||
rel="noopener noreferrer"
|
href={url}
|
||||||
>
|
target="_blank"
|
||||||
{children}
|
rel="noopener noreferrer"
|
||||||
{showIcon && <Icon icon={Briefcase} className="be-indicator-icon mr-1" />}
|
>
|
||||||
<span className="be-indicator-label break-words">Business Feature</span>
|
{showIcon && (
|
||||||
</a>
|
<Icon icon={Briefcase} className="be-indicator-icon mr-1" />
|
||||||
|
)}
|
||||||
|
<span className="be-indicator-label break-words">
|
||||||
|
Business Feature
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children(limitedToBE)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ export function Switch({
|
||||||
/>
|
/>
|
||||||
<span className="slider round before:content-['']" />
|
<span className="slider round before:content-['']" />
|
||||||
</label>
|
</label>
|
||||||
{limitedToBE && <BEFeatureIndicator featureId={featureId} />}
|
{featureId && limitedToBE && <BEFeatureIndicator featureId={featureId} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from 'react';
|
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 { Modal, OnSubmit, openModal } from '@@/modals';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
import { Registry } from '@/react/portainer/registries/types';
|
import { Registry } from '@/react/portainer/registries/types/registry';
|
||||||
|
|
||||||
enum WebhookType {
|
enum WebhookType {
|
||||||
Service = 1,
|
Service = 1,
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGrou
|
||||||
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
|
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { EnvironmentType } from '@/react/portainer/environments/types';
|
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 { useRegistries } from '@/react/portainer/registries/queries/useRegistries';
|
||||||
import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset';
|
import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset';
|
||||||
import { parseRelativePathResponse } from '@/react/portainer/gitops/RelativePathFieldset/utils';
|
import { parseRelativePathResponse } from '@/react/portainer/gitops/RelativePathFieldset/utils';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
import { buildUrl } from '@/react/edge/edge-stacks/queries/buildUrl';
|
import { buildUrl } from '@/react/edge/edge-stacks/queries/buildUrl';
|
||||||
import { DeploymentType, EdgeStack } from '@/react/edge/edge-stacks/types';
|
import { DeploymentType, EdgeStack } from '@/react/edge/edge-stacks/types';
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/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 {
|
export interface UpdateEdgeStackGitPayload {
|
||||||
id: EdgeStack['Id'];
|
id: EdgeStack['Id'];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { RefreshCw } from 'lucide-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 { Select } from '@@/form-components/ReactSelect';
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import axios, {
|
||||||
json2formData,
|
json2formData,
|
||||||
parseAxiosError,
|
parseAxiosError,
|
||||||
} from '@/portainer/services/axios';
|
} 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 { Pair } from '@/react/portainer/settings/types';
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
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 { Pair } from '@/react/portainer/settings/types';
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
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 { Pair } from '@/react/portainer/settings/types';
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
import { AutoUpdateModel } from '@/react/portainer/gitops/types';
|
import { AutoUpdateModel } from '@/react/portainer/gitops/types';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
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 { Pair } from '@/react/portainer/settings/types';
|
||||||
import {
|
import {
|
||||||
GitFormModel,
|
GitFormModel,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
import { withError } from '@/react-tools/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, {
|
import axios, {
|
||||||
json2formData,
|
json2formData,
|
||||||
parseAxiosError,
|
parseAxiosError,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
RelativePathModel,
|
RelativePathModel,
|
||||||
RepoConfigResponse,
|
RepoConfigResponse,
|
||||||
} from '@/react/portainer/gitops/types';
|
} 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';
|
import { EnvVar } from '@@/form-components/EnvironmentVariablesFieldset/types';
|
||||||
|
|
||||||
|
|
|
@ -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 { IngressControllerClassMap } from '../../cluster/ingressClass/types';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { MultiValue } from 'react-select';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
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 { FormControl } from '@@/form-components/FormControl';
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
import { MultiValue } from 'react-select';
|
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 { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { MultiValue } from 'react-select';
|
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 { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { Select } from '@@/form-components/ReactSelect';
|
import { Select } from '@@/form-components/ReactSelect';
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { SchemaOf, array, object, number, string } from 'yup';
|
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<Registry[]> = array(
|
export const registriesValidationSchema: SchemaOf<Registry[]> = array(
|
||||||
object({
|
object({
|
||||||
Id: number().required('Registry ID is required.'),
|
Id: number().required('Registry ID is required.'),
|
||||||
Name: string().required('Registry name is required.'),
|
Name: string().required('Registry name is required.'),
|
||||||
})
|
}) as unknown as SchemaOf<Registry>
|
||||||
|
// the only needed value is actually the id. SchemaOf throw a ts error if we don't cast to SchemaOf<Registry>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||||
import { UserId } from '@/portainer/users/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';
|
import { EnvironmentId } from '../types';
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ export function Option({
|
||||||
<div className="mt-3 flex flex-col text-center">
|
<div className="mt-3 flex flex-col text-center">
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
<h5>{description}</h5>
|
<h5>{description}</h5>
|
||||||
{isLimited && (
|
{featureId && isLimited && (
|
||||||
<BEFeatureIndicator
|
<BEFeatureIndicator
|
||||||
showIcon={false}
|
showIcon={false}
|
||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export { DefaultRegistryAction } from './DefaultRegistryAction';
|
|
||||||
export { DefaultRegistryDomain } from './DefaultRegistryDomain';
|
|
||||||
export { DefaultRegistryName } from './DefaultRegistryName';
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
import { InformationPanel } from '@@/InformationPanel';
|
||||||
|
|
||||||
|
import { RegistriesDatatable } from './RegistriesDatatable';
|
||||||
|
|
||||||
|
export function ListView() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Registries" breadcrumbs="Registry management" reload />
|
||||||
|
|
||||||
|
<InformationPanel title="Information">
|
||||||
|
<span className="small text-muted">
|
||||||
|
View registries via an environment to manage access for user(s) and/or
|
||||||
|
team(s)
|
||||||
|
</span>
|
||||||
|
</InformationPanel>
|
||||||
|
|
||||||
|
<RegistriesDatatable />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { AddButton as BaseAddButton } from '@@/buttons';
|
||||||
|
|
||||||
|
export function AddButton() {
|
||||||
|
return (
|
||||||
|
<BaseAddButton
|
||||||
|
data-cy="registry-addRegistryButton"
|
||||||
|
to="portainer.registries.new"
|
||||||
|
>
|
||||||
|
Add registry
|
||||||
|
</BaseAddButton>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<BaseDeleteButton
|
||||||
|
data-cy="registry-removeRegistryButton"
|
||||||
|
disabled={selectedItems.length === 0}
|
||||||
|
confirmMessage={confirmMessage}
|
||||||
|
onConfirmed={handleDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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}?`;
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<Datatable
|
||||||
|
columns={columns}
|
||||||
|
dataset={query.data || []}
|
||||||
|
isLoading={query.isLoading}
|
||||||
|
settingsManager={tableState}
|
||||||
|
title="Registries"
|
||||||
|
titleIcon={Radio}
|
||||||
|
renderTableActions={(selectedItems) => (
|
||||||
|
<>
|
||||||
|
<DeleteButton selectedItems={selectedItems} />
|
||||||
|
|
||||||
|
<AddButton />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
isRowSelectable={(row) => !!row.original.Id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ export function DefaultRegistryAction() {
|
||||||
Hide for all users
|
Hide for all users
|
||||||
</Button>
|
</Button>
|
||||||
<BEFeatureIndicator featureId={FeatureId.HIDE_DOCKER_HUB_ANONYMOUS} />
|
<BEFeatureIndicator featureId={FeatureId.HIDE_DOCKER_HUB_ANONYMOUS} />
|
||||||
{isLimited ? null : (
|
{isLimited && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
message="This hides the option in any registry dropdown prompts but does not prevent a user from deploying anonymously from Docker Hub directly via YAML.
|
message="This hides the option in any registry dropdown prompts but does not prevent a user from deploying anonymously from Docker Hub directly via YAML.
|
||||||
Note: Docker Hub (anonymous) will continue to show as the ONLY option if there are NO other registries available to the user."
|
Note: Docker Hub (anonymous) will continue to show as the ONLY option if there are NO other registries available to the user."
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
import { BEFeatureIndicator } from '@@/BEFeatureIndicator';
|
||||||
|
|
||||||
|
import { DecoratedRegistry } from '../types';
|
||||||
|
import { RegistryId, RegistryTypes } from '../../../types/registry';
|
||||||
|
|
||||||
|
import { columnHelper } from './helper';
|
||||||
|
import { DefaultRegistryAction } from './DefaultRegistryAction';
|
||||||
|
|
||||||
|
export const actions = columnHelper.display({
|
||||||
|
header: 'Actions',
|
||||||
|
cell: Cell,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nonBrowsableTypes = [
|
||||||
|
RegistryTypes.ANONYMOUS,
|
||||||
|
RegistryTypes.DOCKERHUB,
|
||||||
|
RegistryTypes.QUAY,
|
||||||
|
];
|
||||||
|
|
||||||
|
function Cell({
|
||||||
|
row: { original: item },
|
||||||
|
}: CellContext<DecoratedRegistry, unknown>) {
|
||||||
|
if (!item.Id) {
|
||||||
|
return <DefaultRegistryAction />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BrowseButton registryId={item.Id} registryType={item.Type} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrowseButton({
|
||||||
|
registryId,
|
||||||
|
registryType,
|
||||||
|
environmentId,
|
||||||
|
}: {
|
||||||
|
registryId: RegistryId;
|
||||||
|
registryType: RegistryTypes;
|
||||||
|
environmentId?: EnvironmentId;
|
||||||
|
}) {
|
||||||
|
const canBrowse = !nonBrowsableTypes.includes(registryType);
|
||||||
|
|
||||||
|
if (!canBrowse) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BEFeatureIndicator featureId={FeatureId.REGISTRY_MANAGEMENT}>
|
||||||
|
{(isLimited) => (
|
||||||
|
<Button
|
||||||
|
color="link"
|
||||||
|
as={Link}
|
||||||
|
props={{
|
||||||
|
to: 'portainer.registries.registry.repositories',
|
||||||
|
params: { id: registryId, endpointId: environmentId },
|
||||||
|
}}
|
||||||
|
disabled={isLimited}
|
||||||
|
icon={Search}
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</BEFeatureIndicator>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { DecoratedRegistry } from '../types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<DecoratedRegistry>();
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { actions } from './actions';
|
||||||
|
import { name } from './name';
|
||||||
|
import { url } from './url';
|
||||||
|
|
||||||
|
export const columns = [name, url, actions];
|
|
@ -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<DecoratedRegistry, string>) {
|
||||||
|
return <NameCell item={item} hasLink />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NameCell({
|
||||||
|
item,
|
||||||
|
hasLink,
|
||||||
|
}: {
|
||||||
|
item: DecoratedRegistry;
|
||||||
|
hasLink?: boolean;
|
||||||
|
}) {
|
||||||
|
const isEdgeAdminQuery = useIsEdgeAdmin();
|
||||||
|
|
||||||
|
if (!item.Id) {
|
||||||
|
return <DefaultRegistryName />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isEdgeAdminQuery.isAdmin && hasLink ? (
|
||||||
|
<Link to="portainer.registries.registry" params={{ id: item.Id }}>
|
||||||
|
{item.Name}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
item.Name
|
||||||
|
)}
|
||||||
|
{item.Authentication && (
|
||||||
|
<span className="ml-2 label label-info image-tag">
|
||||||
|
authentication-enabled
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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() : <DefaultRegistryDomain />,
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export { RegistriesDatatable } from './RegistriesDatatable';
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Registry } from '../../types/registry';
|
||||||
|
|
||||||
|
export interface DecoratedRegistry extends Registry {}
|
|
@ -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<Registry['Id']>) =>
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { ListView } from './ListView';
|
|
@ -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 (
|
||||||
|
<Datatable
|
||||||
|
columns={columns}
|
||||||
|
dataset={query.data || []}
|
||||||
|
isLoading={query.isLoading}
|
||||||
|
settingsManager={tableState}
|
||||||
|
title="Registries"
|
||||||
|
titleIcon={Radio}
|
||||||
|
renderTableActions={() => <AddButton />}
|
||||||
|
disableSelect
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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<DecoratedRegistry, unknown>) {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const hasUpdateAccessAuthorizations = useAuthorizations(
|
||||||
|
['PortainerRegistryUpdateAccess'],
|
||||||
|
environmentId,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const canManageAccess =
|
||||||
|
item.Type !== RegistryTypes.ANONYMOUS && hasUpdateAccessAuthorizations;
|
||||||
|
|
||||||
|
if (!item.Id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{canManageAccess && (
|
||||||
|
<Authorized authorizations="PortainerRegistryUpdateAccess">
|
||||||
|
<Button
|
||||||
|
color="link"
|
||||||
|
icon={Users}
|
||||||
|
as={Link}
|
||||||
|
props={{ to: '.access', params: { id: item.Id } }}
|
||||||
|
>
|
||||||
|
Manage access
|
||||||
|
</Button>
|
||||||
|
</Authorized>
|
||||||
|
)}
|
||||||
|
<BrowseButton
|
||||||
|
registryId={item.Id}
|
||||||
|
registryType={item.Type}
|
||||||
|
environmentId={environmentId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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<DecoratedRegistry, string>) {
|
||||||
|
return <NameCell item={item} />;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { EnvironmentRegistriesDatatable } from './EnvironmentRegistriesDatatable';
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
|
||||||
|
import { EnvironmentRegistriesDatatable } from './EnvironmentRegistriesDatatable';
|
||||||
|
|
||||||
|
export function ListView() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Environment registries"
|
||||||
|
breadcrumbs="Registry management"
|
||||||
|
reload
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EnvironmentRegistriesDatatable />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { ListView } from './ListView';
|
|
@ -1,6 +1,9 @@
|
||||||
|
import { EnvironmentId } from '../../environments/types';
|
||||||
import { RegistryId } from '../types/registry';
|
import { RegistryId } from '../types/registry';
|
||||||
|
|
||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
base: () => ['registries'] as const,
|
base: () => ['registries'] as const,
|
||||||
|
list: (environmentId?: EnvironmentId) =>
|
||||||
|
[...queryKeys.base(), { environmentId }] as const,
|
||||||
item: (registryId: RegistryId) => [...queryKeys.base(), registryId] as const,
|
item: (registryId: RegistryId) => [...queryKeys.base(), registryId] as const,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
export type RegistryId = number;
|
|
||||||
export interface Registry {
|
|
||||||
Id: RegistryId;
|
|
||||||
Name: string;
|
|
||||||
}
|
|
|
@ -5,7 +5,7 @@ import { ResourceControlResponse } from '../../access-control/types';
|
||||||
import { RelativePathModel, RepoConfigResponse } from '../../gitops/types';
|
import { RelativePathModel, RepoConfigResponse } from '../../gitops/types';
|
||||||
import { VariableDefinition } from '../../custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
import { VariableDefinition } from '../../custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||||
import { Platform } from '../types';
|
import { Platform } from '../types';
|
||||||
import { RegistryId } from '../../registries/types';
|
import { RegistryId } from '../../registries/types/registry';
|
||||||
import { getDefaultRelativePathModel } from '../../gitops/RelativePathFieldset/types';
|
import { getDefaultRelativePathModel } from '../../gitops/RelativePathFieldset/types';
|
||||||
import { isBE } from '../../feature-flags/feature-flags.service';
|
import { isBE } from '../../feature-flags/feature-flags.service';
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue