fix(service): service related UI issues [EE-4062] (#7943)

pull/9007/head
Chamhaw 2023-05-25 11:59:32 +08:00 committed by GitHub
parent 93866644c6
commit a2f734051c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 136 additions and 98 deletions

View File

@ -8,7 +8,7 @@
<!-- name-input -->
<div class="form-group">
<label for="service_name" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-10">
<div class="col-sm-8">
<input type="text" class="form-control" ng-model="formValues.Name" id="service_name" placeholder="e.g. myService" />
</div>
</div>
@ -18,8 +18,8 @@
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
label-class="col-sm-2"
input-class="col-sm-8"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="true"
@ -30,29 +30,29 @@
<div class="col-sm-12 form-section-title"> Scheduling </div>
<!-- scheduling-mode -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left"> Scheduling mode </label>
<div class="btn-group btn-group-sm" style="margin-left: 20px">
<div>
<label class="control-label col-sm-2 text-left"> Scheduling mode </label>
<div class="btn-group btn-group-sm col-sm-8">
<label class="btn btn-light" ng-model="formValues.Mode" uib-btn-radio="'global'">Global</label>
<label class="btn btn-light" ng-model="formValues.Mode" uib-btn-radio="'replicated'">Replicated</label>
</div>
</div>
</div>
<div class="form-group form-inline" ng-if="formValues.Mode === 'replicated'">
<div class="col-sm-12">
<label class="control-label text-left"> Replicas </label>
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3" style="margin-left: 20px" />
<div>
<label class="control-label col-sm-2 text-left"> Replicas </label>
<div class="col-sm-8">
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3" />
</div>
</div>
</div>
<!-- !scheduling-mode -->
<div class="col-sm-12 form-section-title"> Ports configuration </div>
<!-- port-mapping -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive vertical-center" style="margin-left: 10px" ng-click="addPortBinding()">
<pr-icon icon="'plus'" mode="'alt'"></pr-icon> map additional port
</span>
<label class="control-label col-sm-2 text-left">Port mapping</label>
<div class="col-sm-8 pt-2">
<span class="label label-default interactive vertical-center" ng-click="addPortBinding()"> <pr-icon icon="'plus'" mode="'alt'"></pr-icon> map additional port </span>
</div>
<div class="col-sm-12 form-inline mt-2">
<div ng-repeat="portBinding in formValues.Ports" class="mt-1">

View File

@ -4,7 +4,7 @@
<rd-widget-body classes="no-padding">
<div class="form-inline" style="padding: 10px" authorization="DockerServiceUpdate">
Add a config:
<select class="form-control" ng-options="config.Name for config in configs | orderBy: 'Name'" ng-model="newConfig">
<select class="form-control !h-[30px] !text-[13px]" ng-options="config.Name for config in configs | orderBy: 'Name'" ng-model="newConfig">
<option selected disabled hidden value="">Select a config</option>
</select>
<a class="btn btn-default btn-sm" ng-click="addConfig(service, newConfig)"> <pr-icon icon="'plus'"></pr-icon> add config </a>

View File

@ -2,10 +2,10 @@
<rd-widget>
<rd-widget-header icon="list" title-text="Container spec"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<table class="!mb-0 table">
<tbody>
<tr>
<td>CMD</td>
<td class="w-1/5">CMD</td>
<td
><code ng-if="service.Command">{{ service.Command | command }}</code></td
>

View File

@ -4,7 +4,7 @@
<rd-widget-body classes="no-padding">
<div class="form-inline" style="padding: 10px" authorization="DockerServiceUpdate">
Driver:
<select class="form-control" ng-model="service.LogDriverName" ng-change="updateLogDriverName(service)" ng-disabled="isUpdating">
<select class="form-control !h-[30px] !text-[13px]" ng-model="service.LogDriverName" ng-change="updateLogDriverName(service)" ng-disabled="isUpdating">
<option selected value="">Default logging driver</option>
<option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option>
<option value="none">none</option>
@ -22,7 +22,7 @@
</thead>
<tbody>
<tr ng-repeat="option in service.LogDriverOpts">
<td>
<td class="w-1/2">
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="option.key" ng-disabled="option.added || isUpdating" placeholder="e.g. FOO" />

View File

@ -9,7 +9,7 @@
<p>There are no mounts for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.ServiceMounts.length > 0" classes="no-padding">
<table class="table">
<table class="mb-0 table">
<thead>
<tr>
<th ng-if="isAdmin || allowBindMounts">Type</th>
@ -20,11 +20,11 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="mount in service.ServiceMounts">
<td ng-if="isAdmin || allowBindMounts">
<tr ng-repeat="(index, mount) in service.ServiceMounts">
<td class="!pt-6 !align-top" ng-if="isAdmin || allowBindMounts">
<select
name="mountType"
class="form-control"
class="form-control !h-[30px] !text-[13px]"
ng-model="mount.Type"
ng-change="onChangeMountType(service, mount)"
ng-disabled="isUpdating"
@ -34,47 +34,53 @@
<option value="bind">Bind</option>
</select>
</td>
<td>
<select
class="form-control"
ng-model="mount.Source"
ng-change="updateMount(service, mount)"
ng-options="vol.Id as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
ng-if="mount.Type === 'volume'"
disable-authorization="DockerServiceUpdate"
<td class="!pt-6 !pb-0 !align-top">
<div class="mb-6">
<select
class="form-control !h-[30px] !text-[13px]"
ng-model="mount.Source"
ng-change="updateMount(service, mount)"
ng-options="vol.Id as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
ng-if="mount.Type === 'volume'"
disable-authorization="DockerServiceUpdate"
>
<option selected disabled hidden value="">Select a volume</option>
</select>
<input
type="text"
class="form-control !h-[30px] !text-[13px]"
name=""
ng-model="mount.Source"
placeholder="e.g. /tmp/portainer/data"
ng-change="updateMount(service, mount)"
ng-disabled="isUpdating || (!isAdmin && !allowBindMounts && mount.Type === 'bind')"
ng-if="mount.Type === 'bind'"
/>
</div>
<div class="small text-warning !-mt-6" ng-show="!mount.Source">
<div class="vertical-center"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Source is required. </div></div
>
<option selected disabled hidden value="">Select a volume</option>
</select>
<input
type="text"
class="form-control"
name=""
ng-model="mount.Source"
placeholder="e.g. /tmp/portainer/data"
ng-change="updateMount(service, mount)"
ng-disabled="isUpdating || (!isAdmin && !allowBindMounts && mount.Type === 'bind')"
ng-if="mount.Type === 'bind'"
/>
<div class="col-sm-12 small text-warning" ng-show="!mount.Source"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Source is required. </div>
</td>
<td>
<td class="!pt-6 !pb-0 !align-top">
<input
type="text"
class="form-control"
class="form-control mb-6 !h-[30px] !text-[13px]"
ng-model="mount.Target"
placeholder="e.g. /tmp/portainer/data"
ng-change="updateMount(service, mount)"
ng-disabled="isUpdating"
disable-authorization="DockerServiceUpdate"
/>
<div class="col-sm-12 small text-warning" ng-show="!mount.Target"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Target is required. </div>
<div class="small text-warning !-mt-6" ng-show="!mount.Target">
<div class="vertical-center"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Target is required. </div></div
>
</td>
<td authorization="DockerServiceUpdate">
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating" />
<por-switch-field checked="mount.ReadOnly" disabled="isUpdating" on-change="(toggleMountReadOnly)" index="index"></por-switch-field>
</td>
<td authorization="DockerServiceUpdate">
<span class="input-group-btn">
<button class="btn btn-dangerlight" type="button" ng-click="removeMount(service, $index)" ng-disabled="isUpdating">
<button class="btn btn-dangerlight btn-sm" type="button" ng-click="removeMount(service, $index)" ng-disabled="isUpdating">
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
</span>

View File

@ -23,7 +23,7 @@
<td>
<select
ng-if="network.Editable"
class="form-control"
class="form-control !h-[30px] !rounded !text-[13px]"
ng-model="network.Id"
ng-change="updateNetwork(service)"
ng-options="net.Id as net.Name for net in filterNetworks(swarmNetworks, network)"
@ -42,7 +42,7 @@
</td>
<td ng-if="network.Editable" authorization="DockerServiceUpdate">
<span class="input-group-btn">
<button class="btn btn-dangerlight" type="button" ng-click="removeNetwork(service, $index)" ng-disabled="isUpdating">
<button class="btn btn-sm btn-dangerlight" type="button" ng-click="removeNetwork(service, $index)" ng-disabled="isUpdating">
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
</span>

View File

@ -25,7 +25,7 @@
<tr ng-repeat="portBinding in service.Ports">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon">host</span>
<span class="input-group-addon !leading-none">host</span>
<input
type="number"
class="form-control"
@ -39,7 +39,7 @@
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon">container</span>
<span class="input-group-addon !leading-none">container</span>
<input
type="number"
class="form-control"
@ -54,7 +54,7 @@
<td>
<div class="input-group input-group-sm">
<select
class="selectpicker form-control"
class="selectpicker form-control !rounded"
ng-model="portBinding.Protocol"
ng-change="updatePublishedPort(service, mapping)"
ng-disabled="isUpdating"
@ -68,7 +68,7 @@
<td>
<div class="input-group input-group-sm">
<select
class="selectpicker form-control"
class="selectpicker form-control !rounded"
ng-model="portBinding.PublishMode"
ng-change="updatePublishedPort(service, mapping)"
ng-disabled="isUpdating"
@ -81,7 +81,7 @@
</td>
<td authorization="DockerServiceUpdate">
<span class="input-group-btn">
<button class="btn btn-dangerlight" type="button" ng-click="removePortPublishedBinding(service, $index)" ng-disabled="isUpdating">
<button class="btn btn-sm btn-dangerlight" type="button" ng-click="removePortPublishedBinding(service, $index)" ng-disabled="isUpdating">
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
</span>

View File

@ -9,7 +9,7 @@
<td>
<div class="input-group input-group-sm">
<select
class="selectpicker form-control"
class="selectpicker form-control !rounded"
ng-model="service.RestartCondition"
ng-change="updateServiceAttribute(service, 'RestartCondition')"
disable-authorization="DockerServiceUpdate"

View File

@ -4,7 +4,7 @@
<rd-widget-body classes="no-padding">
<div class="form-inline" style="padding: 10px" authorization="DockerServiceUpdate">
Add a secret:
<select class="form-control" ng-options="secret.Name for secret in secrets | orderBy: 'Name'" ng-model="state.addSecret.secret">
<select class="form-control !h-[30px] !text-[13px]" ng-options="secret.Name for secret in secrets | orderBy: 'Name'" ng-model="state.addSecret.secret">
<option selected disabled hidden value="">Select a secret</option>
</select>
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.3 && state.addSecret.override">

View File

@ -2,7 +2,7 @@
<rd-widget>
<rd-widget-header icon="list" title-text="Update configuration"> </rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<table class="mb-0 table">
<tbody>
<tr>
<td>Update Parallelism</td>
@ -38,8 +38,8 @@
<tr>
<td>Update Failure Action</td>
<td>
<div class="form-group">
<label class="radio-inline">
<div class="form-group !mb-0">
<label class="radio-inline align-baseline">
<input
type="radio"
name="failure_action"
@ -50,7 +50,7 @@
/>
Continue
</label>
<label class="radio-inline">
<label class="radio-inline align-baseline">
<input
type="radio"
name="failure_action"
@ -70,7 +70,7 @@
<tr ng-if="applicationState.endpoint.apiVersion >= 1.29">
<td>Order</td>
<td>
<div class="form-group">
<div class="form-group !mb-0">
<label class="radio-inline">
<input
type="radio"

View File

@ -8,7 +8,6 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg-9 col-md-9 col-xs-9">
<rd-widget>
@ -17,7 +16,7 @@
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td class="w-1/5">Name</td>
<td ng-if="applicationState.endpoint.apiVersion <= 1.24">
<input type="text" class="form-control" ng-model="service.Name" ng-change="updateServiceAttribute(service, 'Name')" ng-disabled="isUpdating" />
</td>
@ -66,26 +65,29 @@
<td>{{ service.Image }}</td>
</tr>
<tr ng-if="isAdmin && applicationState.endpoint.type !== 4">
<td colspan="{{ webhookURL ? '1' : '2' }}">
<por-switch-field
tooltip="'Webhook (or callback URI) used to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service.'"
checked="WebhookExists"
disabled="!isAdmin"
on-change="(onWebhookChange)"
label="'Service webhook'"
></por-switch-field>
</td>
<td ng-if="webhookURL">
<span class="text-muted">{{ webhookURL | truncatelr }}</span>
<button type="button" class="btn btn-sm btn-primary btn-sm space-left" ng-if="webhookURL" ng-click="copyWebhook()">
<span>
<pr-icon icon="'copy'" class-name="'mr-1'"></pr-icon>
Copy link</span
<td>
<div class="inline-flex items-center">
<div> Service webhook </div>
<portainer-tooltip
message="'Webhook (or callback URI) used to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service.'"
>
</button>
<span>
<pr-icon id="copyNotification" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
</portainer-tooltip>
</div>
</td>
<td>
<div class="flex flex-wrap items-center">
<por-switch-field label-class="'!mr-0'" checked="WebhookExists" disabled="disabledWebhookButton(WebhookExists)" on-change="(onWebhookChange)"></por-switch-field>
<span ng-if="webhookURL">
<span class="text-muted">{{ webhookURL | truncatelr }}</span>
<button type="button" class="btn btn-sm btn-primary btn-sm space-left" ng-if="webhookURL" ng-click="copyWebhook()">
<pr-icon icon="'copy'" class-name="'mr-1'"></pr-icon>
Copy link
</button>
<span>
<pr-icon id="copyNotification" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
</span>
</div>
</td>
</tr>
<tr authorization="DockerServiceLogs, DockerServiceUpdate, DockerServiceDelete">

View File

@ -236,6 +236,13 @@ angular.module('portainer.docker').controller('ServiceController', [
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
};
$scope.toggleMountReadOnly = function toggleMountReadOnly(isReadOnly, index) {
$scope.$evalAsync(function () {
updateServiceArray($scope.service, 'ServiceMounts', $scope.service.ServiceMounts);
$scope.service.ServiceMounts[index].ReadOnly = isReadOnly;
});
};
$scope.addNetwork = function addNetwork(service) {
if (!service.Networks) {
service.Networks = [];
@ -334,9 +341,11 @@ angular.module('portainer.docker').controller('ServiceController', [
};
$scope.onWebhookChange = function (enabled) {
enabled = enabled | '';
$scope.$evalAsync(() => {
$scope.updateWebhook($scope.service);
$scope.WebhookExists = enabled;
updateServiceAttribute($scope.service, 'Webhooks', enabled);
});
};
@ -724,6 +733,7 @@ angular.module('portainer.docker').controller('ServiceController', [
$scope.isAdmin = Authentication.isAdmin();
$scope.availableNetworks = data.availableNetworks;
$scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm');
$scope.WebhookExists = false;
const serviceNetworks = _.uniqBy(_.concat($scope.service.Model.Spec.Networks || [], $scope.service.Model.Spec.TaskTemplate.Networks || []), 'Target');
const networks = _.filter(

View File

@ -6,6 +6,15 @@
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
Switch to advanced mode to copy & paste multiple variables
</div>
<div class="col-sm-12 form-inline env-items-list">
<environment-variables-simple-mode-item
ng-repeat="variable in $ctrl.ngModel"
variable="variable"
index="$index"
on-change="($ctrl.onChangeVariable)"
on-remove="($ctrl.remove)"
></environment-variables-simple-mode-item>
</div>
<div class="col-sm-12 environment-variables-simple-mode--actions">
<button type="button" class="btn btn-sm btn-default" ng-click="$ctrl.add()"> <pr-icon icon="'plus'"></pr-icon> Add an environment variable </button>
<button
@ -24,15 +33,6 @@
File too large! Try uploading a file smaller than 1MB
</span>
</div>
<div class="col-sm-12 form-inline env-items-list">
<environment-variables-simple-mode-item
ng-repeat="variable in $ctrl.ngModel"
variable="variable"
index="$index"
on-change="($ctrl.onChangeVariable)"
on-remove="($ctrl.remove)"
></environment-variables-simple-mode-item>
</div>
<div class="col-sm-12 small text-muted" ng-if="$ctrl.ngModel.length > 0 && $ctrl.showHelpMessage">
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
Environment changes will not take effect until redeployment occurs manually or via webhook.

View File

@ -5,6 +5,7 @@ import { SwitchField } from '@@/form-components/SwitchField';
export const switchField = r2a(SwitchField, [
'tooltip',
'checked',
'index',
'label',
'name',
'labelClass',

View File

@ -13,8 +13,9 @@ export interface Props {
checked: boolean;
id: string;
name: string;
onChange(checked: boolean): void;
onChange(checked: boolean, index?: number): void;
index?: number;
className?: string;
dataCy?: string;
disabled?: boolean;
@ -28,6 +29,7 @@ export function Switch({
disabled,
dataCy,
onChange,
index,
featureId,
className,
}: Props) {
@ -47,7 +49,7 @@ export function Switch({
id={id}
checked={checked}
disabled={disabled || limitedToBE}
onChange={({ target: { checked } }) => onChange(checked)}
onChange={({ target: { checked } }) => onChange(checked, index)}
/>
<span className="slider round before:content-['']" data-cy={dataCy} />
</label>

View File

@ -7,6 +7,7 @@ function renderDefault({
checked = false,
label = 'label',
onChange = jest.fn(),
index,
}: Partial<Props> = {}) {
return render(
<SwitchField
@ -14,6 +15,7 @@ function renderDefault({
name={name}
checked={checked}
onChange={onChange}
index={index}
/>
);
}
@ -33,5 +35,17 @@ test('clicking should emit on-change with the opposite value', async () => {
const switchElem = await findByRole('checkbox');
fireEvent.click(switchElem);
expect(onChange).toHaveBeenCalledWith(!checked);
expect(onChange).toHaveBeenCalledWith(!checked, undefined);
});
test('clicking should emit on-change with the opposite value and index', async () => {
const onChange = jest.fn();
const checked = true;
const index = 3;
const { findByRole } = renderDefault({ onChange, checked, index });
const switchElem = await findByRole('checkbox');
fireEvent.click(switchElem);
expect(onChange).toHaveBeenCalledWith(!checked, index);
});

View File

@ -11,8 +11,9 @@ import { Switch } from './Switch';
export interface Props {
label: string;
checked: boolean;
onChange(value: boolean): void;
onChange(value: boolean, index?: number): void;
index?: number;
name?: string;
tooltip?: ComponentProps<typeof Tooltip>['message'];
setTooltipHtmlMessage?: ComponentProps<typeof Tooltip>['setHtmlMessage'];
@ -28,6 +29,7 @@ export function SwitchField({
tooltip,
checked,
label,
index,
name,
labelClass,
fieldClass,
@ -57,6 +59,7 @@ export function SwitchField({
checked={checked}
disabled={disabled}
onChange={onChange}
index={index}
featureId={featureId}
dataCy={dataCy}
/>

View File

@ -47,7 +47,7 @@ export function AccessControlPanelDetails({
<table className="table">
<tbody>
<tr data-cy="access-ownership">
<td>Ownership</td>
<td className="w-1/5">Ownership</td>
<td>
<i
className={clsx(ownershipIcon(ownership), 'space-right')}