mirror of
https://github.com/portainer/portainer.git
synced 2025-08-07 06:45:23 +02:00
refactor(registries): migrate list view to react [EE-4704] (#10687)
This commit is contained in:
parent
9600eb6fa1
commit
f584bf3830
61 changed files with 504 additions and 490 deletions
|
@ -356,8 +356,7 @@ angular
|
|||
url: '/registries',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/registries/registries.html',
|
||||
controller: 'RegistriesController',
|
||||
component: 'registriesView',
|
||||
},
|
||||
},
|
||||
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 { 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'])
|
||||
|
|
|
@ -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',
|
||||
|
|
19
app/portainer/react/views/registries.ts
Normal file
19
app/portainer/react/views/registries.ts
Normal file
|
@ -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();
|
||||
},
|
||||
]);
|
Loading…
Add table
Add a link
Reference in a new issue