Merge pull request #2136 from wknet123/master-ui-aot

Fix issue about UX.
This commit is contained in:
Steven Zou 2017-04-26 22:22:09 +08:00 committed by GitHub
commit fc47d3932c
36 changed files with 329 additions and 167 deletions

View File

@ -97,7 +97,7 @@ script:
- docker-compose -f make/docker-compose.test.yml down
- sudo rm -rf /data/config/*
- ls /data/cert
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.0 NOTARYFLAG=true
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.1 NOTARYFLAG=true
- docker ps
- ./tests/notarytest.sh

View File

@ -91,7 +91,7 @@ NEWCLARITYVERSION=
#clarity parameters
CLARITYIMAGE=vmware/harbor-clarity-ui-builder[:tag]
CLARITYSEEDPATH=/clarity-seed
CLARITYSEEDPATH=/harbor_ui
CLARITYBUILDSCRIPT=/entrypoint.sh
# docker parameters

View File

@ -1,20 +1,20 @@
FROM node:7.5.0
RUN mkdir -p /clarity-seed
RUN mkdir -p /harbor_ui
COPY src/ui_ng/package.json /clarity-seed
COPY src/ui_ng/tslint.json /clarity-seed
COPY src/ui_ng/typings.json /clarity-seed
COPY src/ui_ng/yarn.lock /clarity-seed
COPY make/dev/nodeclarity/angular-cli.json /clarity-seed
COPY src/ui_ng/package.json /harbor_ui
COPY src/ui_ng/tslint.json /harbor_ui
COPY src/ui_ng/typings.json /harbor_ui
COPY src/ui_ng/yarn.lock /harbor_ui
COPY make/dev/nodeclarity/angular-cli.json /harbor_ui
COPY make/dev/nodeclarity/entrypoint.sh /
COPY src/ui_ng/tsconfig-aot.json /clarity-seed
COPY src/ui_ng/rollup-config.js /clarity-seed
COPY src/ui_ng/tsconfig-aot.json /harbor_ui
COPY src/ui_ng/rollup-config.js /harbor_ui
WORKDIR /clarity-seed
WORKDIR /harbor_ui
RUN npm __proxy__ install -g @angular/cli && \
npm __proxy__ install && \
chmod u+x /entrypoint.sh
VOLUME ["/clarity-seed", "/clarity-seed/dist"]
VOLUME ["/harbor_ui", "/harbor_ui/dist"]

View File

@ -1,7 +1,7 @@
{
"project": {
"version": "1.0.0-beta.20-4",
"name": "clarity-seed"
"version": "1.1.0",
"name": "Harbor"
},
"apps": [
{

View File

@ -1,7 +1,7 @@
#!/bin/bash
set -e
cd /clarity-seed
cd /harbor_ui
rm -rf dist/*
npm_proxy=

View File

@ -14,12 +14,28 @@
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)"><clr-icon shape="check" [hidden]="!f.checked"></clr-icon> {{f.description | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)">
<clr-icon shape="check" [hidden]="!f.checked"></clr-icon>
<ng-template [ngIf]="!f.checked"><span style="display: inline-block;width: 16px;"></span></ng-template>
{{f.description | translate}}
</a>
</div>
</clr-dropdown>
<div class="flex-xs-middle">
<clr-icon shape="date"></clr-icon><input type="date" #fromTime (change)="doSearchByTimeRange(fromTime.value, 'begin')">
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doSearchByTimeRange(toTime.value, 'end')">
<clr-icon shape="date"></clr-icon>
<label for="fromDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="fromTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
<input id="fromDateInput" type="date" #fromTime="ngModel" name="from" [(ngModel)]="queryParam.fromTime" dateValidator placeholder="dd/mm/yyyy" (change)="doSearchByStartTime(fromTime.value)">
<span *ngIf="fromTimeInvalid" class="tooltip-content">
{{'AUDIT_LOG.INVALID_DATE' | translate }}
</span>
</label>
<clr-icon shape="date"></clr-icon>
<label for="toDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="toTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
<input id="toDateInput" type="date" #toTime="ngModel" name="to" [(ngModel)]="queryParam.toTime" dateValidator placeholder="dd/mm/yyyy" (change)="doSearchByEndTime(toTime.value)">
<span *ngIf="toTimeInvalid" class="tooltip-content">
{{'AUDIT_LOG.INVALID_DATE' | translate }}
</span>
</label>
</div>
</div>
</div>

View File

@ -11,7 +11,8 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { NgModel } from '@angular/forms';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { AuditLog } from './audit-log';
@ -70,6 +71,17 @@ export class AuditLogComponent implements OnInit {
totalRecordCount: number;
totalPage: number;
@ViewChild('fromTime') fromTimeInput: NgModel;
@ViewChild('toTime') toTimeInput: NgModel;
get fromTimeInvalid(): boolean {
return this.fromTimeInput.errors && this.fromTimeInput.errors.dateValidator && (this.fromTimeInput.dirty || this.fromTimeInput.touched)
}
get toTimeInvalid(): boolean {
return this.toTimeInput.errors && this.toTimeInput.errors.dateValidator && (this.toTimeInput.dirty || this.toTimeInput.touched);
}
constructor(private route: ActivatedRoute, private router: Router, private auditLogService: AuditLogService, private messageHandlerService: MessageHandlerService) {
//Get current user from registered resolver.
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['auditLogResolver']);
@ -79,6 +91,7 @@ export class AuditLogComponent implements OnInit {
this.projectId = +this.route.snapshot.parent.params['id'];
this.queryParam.project_id = this.projectId;
this.queryParam.page_size = this.pageSize;
}
retrieve(state?: State): void {
@ -105,15 +118,29 @@ export class AuditLogComponent implements OnInit {
this.retrieve();
}
doSearchByTimeRange(strDate: string, target: string): void {
let oneDayOffset = 3600 * 24;
switch(target) {
case 'begin':
convertDate(strDate: string): string {
if(/^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/.test(strDate)) {
let parts = strDate.split(/[-\/]/);
strDate = parts[2] /*Year*/ + '-' +parts[1] /*Month*/ + '-' + parts[0] /*Date*/;
}
return strDate;
}
doSearchByStartTime(strDate: string): void {
this.queryParam.begin_timestamp = 0;
if(this.fromTimeInput.valid && strDate){
strDate = this.convertDate(strDate);
this.queryParam.begin_timestamp = new Date(strDate).getTime() / 1000;
break;
case 'end':
}
this.retrieve();
}
doSearchByEndTime(strDate: string): void {
this.queryParam.end_timestamp = 0;
if(this.toTimeInput.valid && strDate) {
strDate = this.convertDate(strDate);
let oneDayOffset = 3600 * 24;
this.queryParam.end_timestamp = new Date(strDate).getTime() / 1000 + oneDayOffset;
break;
}
this.retrieve();
}

View File

@ -42,4 +42,6 @@ export class AuditLog {
keywords: string;
page: number;
page_size: number;
fromTime: string;
toTime: string;
}

View File

@ -21,7 +21,8 @@ import { RecentLogComponent } from './recent-log.component';
imports: [SharedModule],
declarations: [
AuditLogComponent,
RecentLogComponent],
RecentLogComponent
],
providers: [AuditLogService],
exports: [
AuditLogComponent,

View File

@ -4,10 +4,10 @@
<div class="modal-body" style="height: 12.8em; overflow-y: hidden;">
<form #projectForm="ngForm">
<section class="form-block">
<div class="form-group">
<label for="create_project_name" class="col-md-4 form-group-label-override">{{'PROJECT.NAME' | translate}}</label>
<label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="projectName.invalid && (projectName.dirty || projectName.touched)" [class.valid]="projectName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" required minlength="2" #projectName="ngModel" targetExists="PROJECT_NAME">
<div class="form-group" style="padding-left: 135px;">
<label for="create_project_name" class="col-md-3 form-group-label-override">{{'PROJECT.NAME' | translate}}</label>
<label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="projectName.invalid && (projectName.dirty || projectName.touched)" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="38" required minlength="2" #projectName="ngModel" targetExists="PROJECT_NAME">
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_IS_REQUIRED' | translate}}
</span>
@ -19,16 +19,17 @@
</span>
</label>
</div>
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</label>
<div class="form-group" style="padding-left: 135px;">
<label class="col-md-3 form-group-label-override">{{'PROJECT.ACCESS_LEVEL' | translate}}</label>
<div class="checkbox-inline">
<input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public">
<label for="create_project_public"></label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-right" style="top:-8px; left:-8px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content" style="margin-left: 5px;">{{'PROJECT.INLINE_HELP_PUBLIC' | translate }}</span>
</a>
</div>
</div>
<p class="inline-help-public">
{{'PROJECT.INLINE_HELP_PUBLIC' | translate }}
</p>
</section>
</form>
</div>

View File

@ -1,12 +1,3 @@
.inline-help-public {
color: #CCCCCC;
font-size: 12px;
line-height: 1.5em;
letter-spacing: 0.01em;
margin-top: 0;
padding: 0 14px;
}
.form-group-label-override {
font-size: 14px;
font-weight: 400;

View File

@ -1,6 +1,6 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="showRoleInfo">{{'PROJECT.ROLE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>

View File

@ -5,8 +5,4 @@
.option-right {
padding-right: 16px;
margin-top: 18px;
}
.datagrid-foot {
margin-bottom: 0.5px;
}

View File

@ -19,9 +19,9 @@
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let m of members">
<clr-dg-action-overflow [hidden]="m.user_id === currentUser.user_id || !hasProjectAdminRole">
<button class="action-item" (click)="changeRole(m, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="action-item" (click)="changeRole(m, 2)">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="action-item" (click)="changeRole(m, 3)">{{'MEMBER.GUEST' | translate}}</button>
<button class="action-item" [hidden]="m.role_id === 1" (click)="changeRole(m, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="action-item" [hidden]="m.role_id === 2" (click)="changeRole(m, 2)">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="action-item" [hidden]="m.role_id === 3" (click)="changeRole(m, 3)">{{'MEMBER.GUEST' | translate}}</button>
<button class="action-item" (click)="deleteMember(m)">{{'MEMBER.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{m.username}}</clr-dg-cell>

View File

@ -22,7 +22,7 @@ import { MemberService } from './member.service';
import { AddMemberComponent } from './add-member/add-member.component';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { ConfirmationTargets, ConfirmationState } from '../../shared/shared.const';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../shared/shared.const';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
@ -111,8 +111,8 @@ export class MemberComponent implements OnInit, OnDestroy {
ngOnInit() {
//Get projectId from route params snapshot.
this.projectId = +this.route.snapshot.parent.params['id'];
this.currentUser = this.session.getCurrentUser();
//Get current user from registered resolver.
this.currentUser = this.session.getCurrentUser();
let resolverData = this.route.snapshot.parent.data;
if(resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
@ -149,7 +149,8 @@ export class MemberComponent implements OnInit, OnDestroy {
'MEMBER.DELETION_SUMMARY',
m.username,
m.user_id,
ConfirmationTargets.PROJECT_MEMBER
ConfirmationTargets.PROJECT_MEMBER,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}

View File

@ -7,6 +7,10 @@
background-color: #fafafa;
}
.subnav .nav {
padding-left: 0;
}
.role-label {
color: #CCCCCC;
font-size: 14px;

View File

@ -13,7 +13,7 @@
</div>
<div class="option-right">
<div class="select" style="float: left;">
<select (change)="doFilterProjects($event)">
<select (change)="doFilterProjects($event)">
<option value="0">{{projectTypes[0] | translate}}</option>
<option value="1">{{projectTypes[1] | translate}}</option>
</select>

View File

@ -29,7 +29,7 @@ import { Response } from '@angular/http';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { ConfirmationTargets, ConfirmationState } from '../shared/shared.const';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
@ -178,7 +178,8 @@ export class ProjectComponent implements OnInit, OnDestroy {
'PROJECT.DELETION_SUMMARY',
p.name,
p.project_id,
ConfirmationTargets.PROJECT
ConfirmationTargets.PROJECT,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}

View File

@ -19,7 +19,7 @@ import { MessageHandlerService } from '../../shared/message-handler/message-hand
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { ConfirmationTargets, ConfirmationState } from '../../shared/shared.const';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
@ -149,7 +149,8 @@ export class DestinationComponent implements OnInit {
'REPLICATION.DELETION_SUMMARY_TARGET',
target.name,
target.id,
ConfirmationTargets.TARGET);
ConfirmationTargets.TARGET,
ConfirmationButtons.DELETE_CANCEL);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
}

View File

@ -5,4 +5,8 @@
.sub-nav-bg-color {
background-color: #fafafa;
}
.subnav .nav {
padding-left: 0;
}

View File

@ -1,58 +1,62 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="flex-xs-middle option-left">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
<div class="flex-xs-middle option-right">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{currentRuleStatus.description | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let r of ruleStatus" (click)="doFilterPolicyStatus(r.key)"> {{r.description | translate}}</a>
</div>
</clr-dropdown>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)" [currentValue]="search.policyName"></grid-filter>
<a href="javascript:void(0)" (click)="refreshPolicies()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="flex-xs-middle option-left">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
<div class="flex-xs-middle option-right">
<div class="select" style="float: left;">
<select (change)="doFilterPolicyStatus($event)">
<option *ngFor="let r of ruleStatus" value="{{r.key}}">{{r.description | translate}}</option>
</select>
</div>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)" [currentValue]="search.policyName"></grid-filter>
<a href="javascript:void(0)" (click)="refreshPolicies()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-policy [policies]="changedPolicies" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOnePolicy($event)" (editOne)="openEditPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
<list-policy [policies]="changedPolicies" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOnePolicy($event)" (editOne)="openEditPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<h5 class="flex-items-xs-bottom option-left-down" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
<div class="flex-items-xs-bottom option-right-down">
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption] | translate}}</button>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_JOBS_PLACEHOLDER" | translate}}' (filter)="doSearchJobs($event)" [currentValue]="search.repoName" ></grid-filter>
<a href="javascript:void(0)" (click)="refreshJobs()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
<div class="row flex-items-xs-between">
<h5 class="flex-items-xs-bottom option-left-down" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
<div class="flex-items-xs-bottom option-right-down">
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption] | translate}}</button>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_JOBS_PLACEHOLDER" | translate}}' (filter)="doSearchJobs($event)" [currentValue]="search.repoName" ></grid-filter>
<a href="javascript:void(0)" (click)="refreshJobs()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
<div class="row flex-items-xs-right option-right" [hidden]="currentJobSearchOption === 0">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{currentJobStatus.description | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let j of jobStatus" (click)="doFilterJobStatus(j.key)"> {{j.description | translate}}</a>
</div>
</clr-dropdown>
<div class="flex-items-xs-middle">
<clr-icon shape="date"></clr-icon><input type="date" #fromTime (change)="doJobSearchByStartTime(fromTime.value)">
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doJobSearchByEndTime(toTime.value)">
</div>
</div>
<div class="row flex-items-xs-right option-right" [hidden]="currentJobSearchOption === 0">
<div class="select" style="float: left;">
<select (change)="doFilterJobStatus($event)">
<option *ngFor="let j of jobStatus" value="{{j.key}}" [selected]="currentJobStatus.key === j.key">{{j.description | translate}}</option>
</select>
</div>
<div class="flex-items-xs-middle">
<clr-icon shape="date"></clr-icon>
<label for="fromDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="fromTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
<input id="fromDateInput" type="date" #fromTime="ngModel" name="from" [(ngModel)]="search.startTime" dateValidator placeholder="dd/mm/yyyy" (change)="doJobSearchByStartTime(fromTime.value)">
<span *ngIf="fromTimeInvalid" class="tooltip-content">
{{'AUDIT_LOG.INVALID_DATE' | translate }}
</span>
</label>
<clr-icon shape="date"></clr-icon>
<label for="toDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="toTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
<input id="toDateInput" type="date" #toTime="ngModel" name="to" [(ngModel)]="search.endTime" dateValidator placeholder="dd/mm/yyyy" (change)="doJobSearchByEndTime(toTime.value)">
<span *ngIf="toTimeInvalid" class="tooltip-content">
{{'AUDIT_LOG.INVALID_DATE' | translate }}
</span>
</label>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-job [jobs]="changedJobs" [totalPage]="jobsTotalPage" [totalRecordCount]="jobsTotalRecordCount" (paginate)="fetchPolicyJobs($event)"></list-job>
<list-job [jobs]="changedJobs" [totalPage]="jobsTotalPage" [totalRecordCount]="jobsTotalRecordCount" (paginate)="fetchPolicyJobs($event)"></list-job>
</div>
</div>

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NgModel } from '@angular/forms';
import { CreateEditPolicyComponent } from '../shared/create-edit-policy/create-edit-policy.component';
@ -20,6 +21,7 @@ import { MessageHandlerService } from '../shared/message-handler/message-handler
import { ReplicationService } from './replication.service';
import { SessionUser } from '../shared/session-user';
import { Policy } from './policy';
import { Job } from './job';
import { Target } from './target';
@ -27,13 +29,13 @@ import { Target } from './target';
import { State } from 'clarity-angular';
const ruleStatus = [
{ 'key': '', 'description': 'REPLICATION.ALL_STATUS'},
{ 'key': 'all', 'description': 'REPLICATION.ALL_STATUS'},
{ 'key': '1', 'description': 'REPLICATION.ENABLED'},
{ 'key': '0', 'description': 'REPLICATION.DISABLED'}
];
const jobStatus = [
{ 'key': '', 'description': 'REPLICATION.ALL' },
{ 'key': 'all', 'description': 'REPLICATION.ALL' },
{ 'key': 'pending', 'description': 'REPLICATION.PENDING' },
{ 'key': 'running', 'description': 'REPLICATION.RUNNING' },
{ 'key': 'error', 'description': 'REPLICATION.ERROR' },
@ -51,7 +53,9 @@ class SearchOption {
repoName: string = '';
status: string = '';
startTime: string = '';
startTimestamp: string = '';
endTime: string = '';
endTimestamp: string = '';
page: number = 1;
pageSize: number = 5;
}
@ -62,10 +66,10 @@ class SearchOption {
styleUrls: ['./replication.component.css']
})
export class ReplicationComponent implements OnInit {
projectId: number;
search: SearchOption;
search: SearchOption = new SearchOption();
ruleStatus = ruleStatus;
currentRuleStatus: {key: string, description: string};
@ -89,15 +93,25 @@ export class ReplicationComponent implements OnInit {
@ViewChild(CreateEditPolicyComponent)
createEditPolicyComponent: CreateEditPolicyComponent;
@ViewChild('fromTime') fromTimeInput: NgModel;
@ViewChild('toTime') toTimeInput: NgModel;
get fromTimeInvalid(): boolean {
return this.fromTimeInput.errors && this.fromTimeInput.errors.dateValidator && (this.fromTimeInput.dirty || this.fromTimeInput.touched);
}
get toTimeInvalid(): boolean {
return this.toTimeInput.errors && this.toTimeInput.errors.dateValidator && (this.toTimeInput.dirty || this.toTimeInput.touched);
}
constructor(
private messageHandlerService: MessageHandlerService,
private replicationService: ReplicationService,
private route: ActivatedRoute) {
private route: ActivatedRoute) {
}
ngOnInit(): void {
this.projectId = +this.route.snapshot.parent.params['id'];
this.search = new SearchOption();
this.currentRuleStatus = this.ruleStatus[0];
this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0;
@ -148,7 +162,7 @@ export class ReplicationComponent implements OnInit {
}
this.replicationService
.listJobs(this.search.policyId, this.search.status, this.search.repoName,
this.search.startTime, this.search.endTime, this.search.page, this.search.pageSize)
this.search.startTimestamp, this.search.endTimestamp, this.search.page, this.search.pageSize)
.subscribe(
response=>{
this.jobsTotalRecordCount = response.headers.get('x-total-count');
@ -171,9 +185,9 @@ export class ReplicationComponent implements OnInit {
if(policy) {
this.search.policyId = policy.id;
this.search.repoName = '';
this.search.status = ''
this.search.status = '';
this.currentJobSearchOption = 0;
this.currentJobStatus = { 'key': '', 'description': 'REPLICATION.ALL'};
this.currentJobStatus = { 'key': 'all', 'description': 'REPLICATION.ALL' };
this.fetchPolicyJobs();
}
}
@ -183,19 +197,28 @@ export class ReplicationComponent implements OnInit {
this.retrievePolicies();
}
doFilterPolicyStatus(status: string) {
this.currentRuleStatus = this.ruleStatus.find(r=>r.key === status);
if(status.trim() === '') {
this.changedPolicies = this.policies;
} else {
this.changedPolicies = this.policies.filter(policy=>policy.enabled === +this.currentRuleStatus.key);
doFilterPolicyStatus($event: any) {
if ($event && $event.target && $event.target["value"]) {
let status = $event.target["value"];
this.currentRuleStatus = this.ruleStatus.find(r=>r.key === status);
if(this.currentRuleStatus.key === 'all') {
this.changedPolicies = this.policies;
} else {
this.changedPolicies = this.policies.filter(policy=>policy.enabled === +this.currentRuleStatus.key);
}
}
}
doFilterJobStatus(status: string) {
this.currentJobStatus = this.jobStatus.find(r=>r.key === status);
this.search.status = status;
this.doSearchJobs(this.search.repoName);
doFilterJobStatus($event: any) {
if ($event && $event.target && $event.target["value"]) {
let status = $event.target["value"];
this.currentJobStatus = this.jobStatus.find(r=>r.key === status);
if(this.currentJobStatus.key === 'all') {
status = '';
}
this.search.status = status;
this.doSearchJobs(this.search.repoName);
}
}
doSearchJobs(repoName: string) {
@ -222,20 +245,30 @@ export class ReplicationComponent implements OnInit {
(option === 1) ? this.currentJobSearchOption = 0 : this.currentJobSearchOption = 1;
}
convertDate(strDate: string): string {
if(/^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/.test(strDate)) {
let parts = strDate.split(/[-\/]/);
strDate = parts[2] /*Year*/ + '-' +parts[1] /*Month*/ + '-' + parts[0] /*Date*/;
}
return strDate;
}
doJobSearchByStartTime(strDate: string) {
if(!strDate || strDate.trim() === '') {
strDate = 0 + '';
}
(strDate === '0') ? this.search.startTime = '' : this.search.startTime = (new Date(strDate).getTime() / 1000) + '';
this.search.startTimestamp = '';
if(this.fromTimeInput.valid && strDate) {
strDate = this.convertDate(strDate);
this.search.startTimestamp = new Date(strDate).getTime() / 1000 + '';
}
this.fetchPolicyJobs();
}
doJobSearchByEndTime(strDate: string) {
if(!strDate || strDate.trim() === '') {
strDate = 0 + '';
this.search.endTimestamp = '';
if(this.toTimeInput.valid && strDate) {
strDate = this.convertDate(strDate);
let oneDayOffset = 3600 * 24;
this.search.endTimestamp = (new Date(strDate).getTime() / 1000 + oneDayOffset) + '';
}
let oneDayOffset = 3600 * 24;
(strDate === '0') ? this.search.endTime = '' : this.search.endTime = (new Date(strDate).getTime() / 1000 + oneDayOffset) + '';
this.fetchPolicyJobs();
}
}

View File

@ -18,7 +18,7 @@ import { RepositoryService } from './repository.service';
import { Repository } from './repository';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { ConfirmationState, ConfirmationTargets } from '../shared/shared.const';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
@ -115,7 +115,8 @@ export class RepositoryComponent implements OnInit {
'REPOSITORY.DELETION_SUMMARY_REPO',
repoName,
repoName,
ConfirmationTargets.REPOSITORY);
ConfirmationTargets.REPOSITORY,
ConfirmationButtons.DELETE_CANCEL);
this.deletionDialogService.openComfirmDialog(message);
}

View File

@ -16,7 +16,7 @@ import { ActivatedRoute } from '@angular/router';
import { RepositoryService } from '../repository.service';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { ConfirmationTargets, ConfirmationState } from '../../shared/shared.const';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../shared/shared.const';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
@ -155,25 +155,25 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
deleteTag(tag: TagView) {
if (tag) {
let titleKey: string, summaryKey: string, content: string, confirmOnly: boolean;
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
if (tag.signed) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
confirmOnly = true;
buttons = ConfirmationButtons.CLOSE;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.tag;
} else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tag.tag;
confirmOnly = false;
}
let message = new ConfirmationMessage(
titleKey,
summaryKey,
content,
tag,
ConfirmationTargets.TAG);
message.confirmOnly = confirmOnly;
ConfirmationTargets.TAG,
buttons);
this.deletionDialogService.openComfirmDialog(message);
}
}

View File

@ -6,8 +6,21 @@
</div>
<div class="confirmation-content">{{dialogContent}}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" *ngIf="!confirmOnly" (click)="cancel()">{{'BUTTON.NO' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{ buttonKey | translate}}</button>
<div class="modal-footer" [ngSwitch]="buttons">
<ng-template [ngSwitchCase]="0">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{ 'BUTTON.CONFIRM' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="1">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.NO' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{ 'BUTTON.YES' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="2">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-danger" (click)="confirm()">{{ 'BUTTON.DELETE' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="3">
<button type="button" class="btn btn-primary" (click)="cancel()">{{'BUTTON.CLOSE' | translate}}</button>
</ng-template>
</div>
</clr-modal>

View File

@ -18,7 +18,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ConfirmationDialogService } from './confirmation-dialog.service';
import { ConfirmationMessage } from './confirmation-message';
import { ConfirmationAcknowledgement } from './confirmation-state-message';
import { ConfirmationState, ConfirmationTargets } from '../shared.const';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared.const';
@Component({
selector: 'confiramtion-dialog',
@ -30,10 +30,9 @@ export class ConfirmationDialogComponent implements OnDestroy {
opened: boolean = false;
dialogTitle: string = "";
dialogContent: string = "";
buttonKey: string = 'BUTTON.OK';
confirmOnly: boolean = false;
message: ConfirmationMessage;
annouceSubscription: Subscription;
buttons: ConfirmationButtons;
constructor(
private confirmationService: ConfirmationDialogService,
@ -42,11 +41,10 @@ export class ConfirmationDialogComponent implements OnDestroy {
this.dialogTitle = msg.title;
this.dialogContent = msg.message;
this.message = msg;
this.confirmOnly = this.message.confirmOnly;
this.buttonKey = this.confirmOnly ? 'BUTTON.CLOSE' : 'BUTTON.OK';
this.translate.get(this.dialogTitle).subscribe((res: string) => this.dialogTitle = res);
this.translate.get(this.dialogContent, { 'param': msg.param }).subscribe((res: string) => this.dialogContent = res);
//Open dialog
this.buttons = msg.buttons;
this.open();
});
}

View File

@ -11,20 +11,21 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { ConfirmationTargets } from '../../shared/shared.const';
import { ConfirmationTargets, ConfirmationButtons } from '../../shared/shared.const';
export class ConfirmationMessage {
public constructor(title: string, message: string, param: string, data: any, targetId: ConfirmationTargets) {
public constructor(title: string, message: string, param: string, data: any, targetId: ConfirmationTargets, buttons?: ConfirmationButtons) {
this.title = title;
this.message = message;
this.data = data;
this.targetId = targetId;
this.param = param;
this.buttons = buttons ? buttons : ConfirmationButtons.CONFIRM_CANCEL;
}
title: string;
message: string;
data: any = {};//default is empty
targetId: ConfirmationTargets = ConfirmationTargets.EMPTY;
param: string;
confirmOnly: boolean;
buttons: ConfirmationButtons;
}

View File

@ -0,0 +1,50 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, OnChanges, Input, SimpleChanges } from '@angular/core';
import { NG_VALIDATORS, Validator, Validators, ValidatorFn, AbstractControl } from '@angular/forms';
@Directive({
selector: '[dateValidator]',
providers: [{provide: NG_VALIDATORS, useExisting: DateValidatorDirective, multi: true}]
})
export class DateValidatorDirective implements Validator, OnChanges {
@Input() dateValidator: string;
private valFn = Validators.nullValidator;
ngOnChanges(changes: SimpleChanges): void {
const change = changes['dateValidator'];
if (change) {
this.valFn = dateValidator();
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: AbstractControl): {[key: string]: any} {
return this.valFn(control);
}
}
export function dateValidator(): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
let controlValue = control.value;
let valid = true;
if(controlValue) {
const regYMD=/^(19|20)\d\d([- /.])(0[1-9]|1[012])\2(0[1-9]|[12][0-9]|3[01])$/g;
const regDMY=/^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/g;
valid = (regYMD.test(controlValue) || regDMY.test(controlValue));
}
return valid ? null : {'dateValidator': { value: controlValue }};
};
}

View File

@ -19,7 +19,7 @@ import { Policy } from '../../replication/policy';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { ConfirmationState, ConfirmationTargets } from '../../shared/shared.const';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../../shared/shared.const';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
@ -135,7 +135,8 @@ export class ListPolicyComponent implements OnDestroy {
'REPLICATION.DELETION_SUMMARY',
policy.name,
policy.id,
ConfirmationTargets.POLICY);
ConfirmationTargets.POLICY,
ConfirmationButtons.DELETE_CANCEL);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}

View File

@ -1,6 +1,6 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let p of projects" [clrDgItem]="p">

View File

@ -67,6 +67,9 @@ export const CookieKeyOfAdmiral = "admiral.endpoint.latest";
export const enum ConfirmationState {
NA, CONFIRMED, CANCEL
}
export const enum ConfirmationButtons {
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE
}
export const ProjectTypes = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS' };
export const RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' };

View File

@ -54,6 +54,7 @@ import { MessageHandlerService } from './message-handler/message-handler.service
import { EmailValidatorDirective } from './email.directive';
import { GaugeComponent } from './gauge/gauge.component';
import { StatisticHandler } from './statictics/statistic-handler.service';
import { DateValidatorDirective } from '../shared/date-validator.directive';
@NgModule({
imports: [
@ -78,7 +79,8 @@ import { StatisticHandler } from './statictics/statistic-handler.service';
ListProjectROComponent,
ListRepositoryROComponent,
EmailValidatorDirective,
GaugeComponent
GaugeComponent,
DateValidatorDirective
],
exports: [
CoreModule,
@ -99,7 +101,8 @@ import { StatisticHandler } from './statictics/statistic-handler.service';
ListProjectROComponent,
ListRepositoryROComponent,
EmailValidatorDirective,
GaugeComponent
GaugeComponent,
DateValidatorDirective
],
providers: [
SessionService,

View File

@ -21,7 +21,7 @@ import { NewUserModalComponent } from './new-user-modal.component';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { ConfirmationState, ConfirmationTargets } from '../shared/shared.const'
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const'
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { SessionService } from '../shared/session.service';
@ -188,7 +188,8 @@ export class UserComponent implements OnInit, OnDestroy {
"USER.DELETION_SUMMARY",
user.username,
user,
ConfirmationTargets.USER
ConfirmationTargets.USER,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(msg);
}

View File

@ -124,6 +124,7 @@
"NAME": "Project Name",
"ROLE": "Role",
"PUBLIC_OR_PRIVATE": "Public",
"ACCESS_LEVEL": "Access Level",
"REPO_COUNT": "Repositories Count",
"CREATION_TIME": "Creation Time",
"PUBLIC": "Public",
@ -197,7 +198,8 @@
"ADVANCED": "Advanced",
"SIMPLE": "Simple",
"ITEMS": "item(s)",
"FILTER_PLACEHOLDER": "Filter Logs"
"FILTER_PLACEHOLDER": "Filter Logs",
"INVALID_DATE": "Invalid date."
},
"REPLICATION": {
"REPLICATION_RULE": "Replication Rule",
@ -265,7 +267,8 @@
"CANNOT_EDIT": "Replication rule cannot be changed while it is enabled.",
"POLICY_ALREADY_EXISTS": "Replication rule already exists.",
"FAILED_TO_DELETE_POLICY_ENABLED": "Cannot delete rule: rule has unfinished job(s) or rule is enabled.",
"FOUND_ERROR_IN_JOBS": "Found errors in the replication job(s), please check."
"FOUND_ERROR_IN_JOBS": "Found errors in the replication job(s), please check.",
"INVALID_DATE": "Invalid date."
},
"DESTINATION": {
"NEW_ENDPOINT": "New Endpoint",

View File

@ -124,6 +124,7 @@
"NAME": "Nombre del Proyecto",
"ROLE": "Rol",
"PUBLIC_OR_PRIVATE": "Público",
"ACCESS_LEVEL": "Nivel de acceso",
"REPO_COUNT": "Contador de repositorios",
"CREATION_TIME": "Fecha de creación",
"PUBLIC": "Público",
@ -197,7 +198,8 @@
"ADVANCED": "Avanzado",
"SIMPLE": "Simple",
"ITEMS": "elemento(s)",
"FILTER_PLACEHOLDER": "Filtrar logs"
"FILTER_PLACEHOLDER": "Filtrar logs",
"INVALID_DATE": "Fecha invalida."
},
"REPLICATION": {
"REPLICATION_RULE": "Reglas de Replicación",
@ -265,7 +267,8 @@
"CANNOT_EDIT": "La regla de replicación no se puede cambiar mientras esté activa.",
"POLICY_ALREADY_EXISTS": "La regla de replicación ya existe.",
"FAILED_TO_DELETE_POLICY_ENABLED": "No se puede eliminar la regla: tiene trabajo(s) sin finalizar o está activa.",
"FOUND_ERROR_IN_JOBS": "Se han encontrado errores en el trabajo de replicación. Por favor, compruébelos."
"FOUND_ERROR_IN_JOBS": "Se han encontrado errores en el trabajo de replicación. Por favor, compruébelos.",
"INVALID_DATE": "Fecha invalida."
},
"DESTINATION": {
"NEW_ENDPOINT": "Nuevo Endpoint",

View File

@ -124,6 +124,7 @@
"NAME": "项目名称",
"ROLE": "角色",
"PUBLIC_OR_PRIVATE": "公开",
"ACCESS_LEVEL": "访问级别",
"REPO_COUNT": "镜像仓库数",
"CREATION_TIME": "创建时间",
"PUBLIC": "公开",
@ -197,7 +198,8 @@
"ADVANCED": "高级检索",
"SIMPLE": "简单检索",
"ITEMS": "条记录",
"FILTER_PLACEHOLDER": "过滤日志"
"FILTER_PLACEHOLDER": "过滤日志",
"INVALID_DATE": "无效日期。"
},
"REPLICATION": {
"REPLICATION_RULE": "复制规则",
@ -265,7 +267,8 @@
"CANNOT_EDIT": "当复制规则启用时无法修改。",
"POLICY_ALREADY_EXISTS": "规则已存在。",
"FAILED_TO_DELETE_POLICY_ENABLED": "删除复制规则失败: 仍有未完成的任务或规则未停用。",
"FOUND_ERROR_IN_JOBS": "复制任务中包含错误,请检查。"
"FOUND_ERROR_IN_JOBS": "复制任务中包含错误,请检查。",
"INVALID_DATE": "无效日期。"
},
"DESTINATION": {
"NEW_ENDPOINT": "新建目标",