Commit a7c7a8e6 authored by Yogi_Wang's avatar Yogi_Wang
Browse files

Upgrade angualr from 7.1.3 to 8.2.0 and clarity from 1.0 to 2.2


Signed-off-by: default avatarYogi_Wang <yawang@vmware.com>
Signed-off-by: default avatarMeina Zhou <meinaz@vmware.com>
Signed-off-by: default avatarsshijun <sshijun@vmware.com>
parent fcc30136
......@@ -20,7 +20,8 @@ matrix:
- go: 1.12.5
env:
- OFFLINE=true
- node_js: 10.16.2
- language: node_js
node_js: 10.16.2
env:
- UI_UT=true
env:
......
......@@ -6,6 +6,7 @@ COPY ./LICENSE /portal_src
WORKDIR /build_dir
RUN cp -r /portal_src/* /build_dir \
&& ls -la \
&& apt-get update \
......@@ -14,7 +15,7 @@ RUN cp -r /portal_src/* /build_dir \
&& npm install \
&& npm run build_lib \
&& npm run link_lib \
&& npm run release
&& node --max_old_space_size=8192 'node_modules/@angular/cli/bin/ng' build --prod
FROM photon:2.0
......
......@@ -26,6 +26,7 @@
"node_modules/@clr/ui/clr-ui.min.css",
"node_modules/swagger-ui/dist/swagger-ui.css",
"node_modules/prismjs/themes/prism-solarizedlight.css",
"src/global.scss",
"src/styles.css"
],
"scripts": [
......
......@@ -4,11 +4,6 @@
"deleteDestPath": false,
"lib": {
"entryFile": "index.ts",
"externals": {
"@ngx-translate/core": "ngx-translate-core",
"@ngx-translate/core/index": "ngx-translate-core",
"ngx-markdown": "ngx-markdown"
},
"umdModuleIds": {
"@clr/angular" : "angular",
"ngx-markdown" : "ngxMarkdown",
......
......@@ -3,11 +3,6 @@
"dest": "./dist",
"lib": {
"entryFile": "index.ts",
"externals": {
"@ngx-translate/core": "ngx-translate-core",
"@ngx-translate/core/index": "ngx-translate-core",
"ngx-markdown": "ngx-markdown"
},
"umdModuleIds": {
"@clr/angular" : "angular",
"ngx-markdown" : "ngxMarkdown",
......
{
"name": "@harbor/ui",
"version": "1.10.0",
"lockfileVersion": 1
}
{
"name": "@harbor/ui",
"version": "1.9.0",
"description": "Harbor shared UI components based on Clarity and Angular7",
"version": "1.10.0",
"description": "Harbor shared UI components based on Clarity and Angular8",
"author": "CNCF",
"module": "index.js",
"main": "bundles/harborui.umd.min.js",
......@@ -19,26 +19,26 @@
},
"homepage": "https://github.com/vmware/harbor#readme",
"peerDependencies": {
"@angular/animations": "^7.1.3",
"@angular/common": "^7.1.3",
"@angular/compiler": "^7.1.3",
"@angular/core": "^7.1.3",
"@angular/forms": "^7.1.3",
"@angular/http": "^7.1.3",
"@angular/platform-browser": "^7.1.3",
"@angular/platform-browser-dynamic": "^7.1.3",
"@angular/router": "^7.1.3",
"@angular/animations": "^8.2.0",
"@angular/common": "^8.2.0",
"@angular/compiler": "^8.2.0",
"@angular/core": "^8.2.0",
"@angular/forms": "^8.2.0",
"@angular/http": "^8.2.0",
"@angular/platform-browser": "^8.2.0",
"@angular/platform-browser-dynamic": "^8.2.0",
"@angular/router": "^8.2.0",
"@ngx-translate/core": "^10.0.2",
"@ngx-translate/http-loader": "^3.0.1",
"@webcomponents/custom-elements": "^1.1.3",
"@clr/angular": "^1.0.0",
"@clr/ui": "^1.0.0",
"@clr/icons": "^1.0.0",
"@clr/angular": "^2.1.0",
"@clr/icons": "^2.1.0",
"@clr/ui": "^2.1.0",
"core-js": "^2.5.4",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",
"ngx-markdown": "^6.2.0",
"ngx-markdown": "^8.1.0",
"rxjs": "^6.3.3",
"ts-helpers": "^1.1.1",
"web-animations-js": "^2.2.1",
......
......@@ -100,6 +100,7 @@ export class Configuration {
oidc_scope?: StringValueItem;
count_per_project: NumberValueItem;
storage_per_project: NumberValueItem;
cfg_expiration: NumberValueItem;
public constructor() {
this.auth_mode = new StringValueItem("db_auth", true);
this.project_creation_restriction = new StringValueItem("everyone", true);
......
......@@ -9,6 +9,7 @@ import { GcViewModelFactory } from './gc.viewmodel.factory';
import { CronScheduleComponent } from '../../cron-schedule/cron-schedule.component';
import { CronTooltipComponent } from "../../cron-schedule/cron-tooltip/cron-tooltip.component";
import { of } from 'rxjs';
import { GcJobData } from './gcLog';
describe('GcComponent', () => {
let component: GcComponent;
......@@ -18,13 +19,17 @@ describe('GcComponent', () => {
systemInfoEndpoint: "/api/system/gc"
};
let mockSchedule = [];
let mockJobs = [
let mockJobs: GcJobData[] = [
{
id: 22222,
schedule: null,
job_status: 'string',
creation_time: new Date(),
update_time: new Date(),
creation_time: new Date().toDateString(),
update_time: new Date().toDateString(),
job_name: 'string',
job_kind: 'string',
job_uuid: 'string',
delete: false
}
];
let spySchedule: jasmine.Spy;
......
......@@ -32,7 +32,7 @@ export class GcComponent implements OnInit {
getText = 'CONFIG.GC';
getLabelCurrent = 'GC.CURRENT_SCHEDULE';
@Output() loadingGcStatus = new EventEmitter<boolean>();
@ViewChild(CronScheduleComponent)
@ViewChild(CronScheduleComponent, {static: false})
CronScheduleComponent: CronScheduleComponent;
constructor(
private gcRepoService: GcRepoService,
......
......@@ -4,79 +4,73 @@
<div class="modal-body">
<label>{{defaultTextsObj.setQuota}}</label>
<form #quotaForm="ngForm" class="clr-form-compact"
<form #quotaForm="ngForm" class=" clr-form clr-form-horizontal"
[class.clr-form-compact-common]="!defaultTextsObj.isSystemDefaultQuota">
<div class="form-group">
<label for="count" class="required">{{ defaultTextsObj.countQuota | translate}}</label>
<label for="count" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-lg tooltip-top-right mr-3px"
[class.invalid]="countInput.invalid && (countInput.dirty || countInput.touched)">
<input name="count" type="text" #countInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.countLimit" pattern="(^-1$)|(^([1-9]+)([0-9]+)*$)" required id="count"
size="40">
<span class="tooltip-content">
{{ 'PROJECT.COUNT_QUOTA_TIP' | translate }}
</span>
</label>
<div class="select-div"></div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right mr-0">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</a>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.warning]="isWarningColor(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)"
[class.danger]="isDangerColor(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)">
<progress value="{{countInput.invalid || +quotaHardLimitValue.countLimit===-1?0:quotaHardLimitValue.countUsed}}"
max="{{countInput.invalid?100:quotaHardLimitValue.countLimit}}" data-displayval="100%"></progress>
</div>
<label class="progress-label">{{ quotaHardLimitValue?.countUsed }} {{ 'QUOTA.OF' | translate }}
{{ countInput?.valid?+quotaHardLimitValue?.countLimit===-1 ? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.countLimit:('QUOTA.INVALID_INPUT' | translate)}}
<clr-input-container>
<label class="left-label required" for="storage">{{ defaultTextsObj?.storageQuota | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</clr-tooltip-content>
</clr-tooltip>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.warning]="isWarningColor(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)"
[class.danger]="isDangerColor(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)">
<progress value="{{countInput.invalid || +quotaHardLimitValue.countLimit===-1?0:quotaHardLimitValue.countUsed}}"
max="{{countInput.invalid?100:quotaHardLimitValue.countLimit}}" data-displayval="100%"></progress>
</div>
<label class="progress-label">{{ quotaHardLimitValue?.countUsed }} {{ 'QUOTA.OF' | translate }}
{{ countInput?.valid?+quotaHardLimitValue?.countLimit===-1 ? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.countLimit:('QUOTA.INVALID_INPUT' | translate)}}
</label>
</div>
</label>
</div>
</div>
<div class="form-group">
<label for="storage" class="required">{{ defaultTextsObj?.storageQuota | translate}}</label>
<label for="storage" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-lg mr-3px tooltip-top-right"
[class.invalid]="(storageInput.invalid && (storageInput.dirty || storageInput.touched))||storageInput.errors">
<input name="storage" type="text" #storageInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.storageLimit"
id="storage" size="40">
<span class="tooltip-content">
{{ 'PROJECT.STORAGE_QUOTA_TIP' | translate }}
</span>
</label>
<div class="select-div">
<select clrSelect name="storageUnit" [(ngModel)]="quotaHardLimitValue.storageUnit">
<ng-template ngFor let-quotaUnit [ngForOf]="quotaUnits" let-i="index">
<option *ngIf="i>1" [value]="quotaUnit.UNIT">{{ quotaUnit?.UNIT }}</option>
</ng-template>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right mr-0">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</a>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.danger]="isDangerColor(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)"
[class.warning]="isWarningColor(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)">
<progress value="{{storageInput.invalid || +quotaHardLimitValue.storageLimit === -1 ?0:quotaHardLimitValue.storageUsed}}"
max="{{storageInput.invalid?0:getByte(+quotaHardLimitValue.storageLimit, quotaHardLimitValue.storageUnit)}}"
data-displayval="100%"></progress>
</div>
<label class="progress-label">
<!-- the comments of progress , when storageLimit !=-1 get integet and unit in hard storage and used storage;and the unit of used storage <= the unit of hard storage
the other : get suitable number and unit-->
{{ +quotaHardLimitValue.storageLimit !== -1 ?(getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partNumberUsed
+ getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partCharacterUsed) : getSuitableUnit(quotaHardLimitValue.storageUsed)}}
{{ 'QUOTA.OF' | translate }}
{{ storageInput?.valid? +quotaHardLimitValue?.storageLimit ===-1? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.storageLimit :('QUOTA.INVALID_INPUT' | translate)}}
{{+quotaHardLimitValue?.storageLimit ===-1?'':quotaHardLimitValue?.storageUnit }}
<input clrInput type="text" name="count" #countInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.countLimit" pattern="(^-1$)|(^([1-9]+)([0-9]+)*$)" required id="count"
size="40" />
<clr-control-error>{{ 'PROJECT.COUNT_QUOTA_TIP' | translate }}</clr-control-error>
</clr-input-container>
<clr-input-container>
<label for="count" class="left-label required">{{ defaultTextsObj.countQuota | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</clr-tooltip-content>
</clr-tooltip>
<div class="clr-select-wrapper">
<select id="select-error" class="clr-select" name="storageUnit" [(ngModel)]="quotaHardLimitValue.storageUnit">
<ng-template ngFor let-quotaUnit [ngForOf]="quotaUnits" let-i="index">
<option *ngIf="i>1" [value]="quotaUnit.UNIT">{{ quotaUnit?.UNIT }}</option>
</ng-template>
</select>
</div>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.danger]="isDangerColor(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)"
[class.warning]="isWarningColor(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)" >
<progress value="{{storageInput.invalid || +quotaHardLimitValue.storageLimit === -1 ?0:quotaHardLimitValue.storageUsed}}"
max="{{storageInput.invalid?0:getByte(+quotaHardLimitValue.storageLimit, quotaHardLimitValue.storageUnit)}}"
data-displayval="100%"></progress>
</div>
<label class="progress-label">
<!-- the comments of progress , when storageLimit !=-1 get integet and unit in hard storage and used storage;and the unit of used storage <= the unit of hard storage
the other : get suitable number and unit-->
{{ +quotaHardLimitValue.storageLimit !== -1 ?(getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partNumberUsed
+ getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partCharacterUsed) : getSuitableUnit(quotaHardLimitValue.storageUsed)}}
{{ 'QUOTA.OF' | translate }}
{{ storageInput?.valid? +quotaHardLimitValue?.storageLimit ===-1? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.storageLimit :('QUOTA.INVALID_INPUT' | translate)}}
{{+quotaHardLimitValue?.storageLimit ===-1?'':quotaHardLimitValue?.storageUnit }}
</label>
</div>
</label>
</div>
</div>
<input clrInput name="storage" type="text" #storageInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.storageLimit"
id="storage" size="40"/>
<clr-control-error>{{ 'PROJECT.STORAGE_QUOTA_TIP' | translate }}</clr-control-error>
</clr-input-container>
</form>
</div>
<div class="modal-footer">
......
......@@ -3,44 +3,61 @@
}
.modal-body {
outline: none;
padding-top: 0.8rem;
overflow-y: visible;
overflow-x: visible;
.clr-form-compact {
div.form-group {
padding-left: 8.5rem;
.clr-form {
.left-label {
width: 9.5rem;
}
.mr-3px {
margin-right: 3px;
}
.mr-3px {
margin-right: 3px;
}
.quota-input {
width: 2rem;
padding-right: 0.8rem;
}
.left-label {
position: relative;
}
.select-div {
width: 2.5rem;
.quota-input {
width: 1.5rem;
margin-left: 1rem;
margin-right: 0.2rem;
}
::ng-deep .clr-form-control {
margin-top: 0.28rem;
.clr-validate-icon {
margin-left: -0.6rem;
}
.select-div {
width: 2.5rem;
}
select {
padding-right: 15px;
}
}
}
.clr-select-wrapper {
position: absolute;
right: -5rem;
top: -0.08rem;
}
}
.clr-form-compact-common {
div.form-group {
padding-left: 6rem;
.left-label {
width: 7.5rem;
}
.quota-input {
margin-left: 0rem;
}
.select-div {
width: 1.6rem;
}
.select-div {
width: 1.6rem;
}
.clr-select-wrapper {
right: -4rem;
}
}
}
......@@ -50,35 +67,26 @@
}
.progress-div {
position: relative;
position: absolute;
padding-right: 0.6rem;
width: 9rem;
}
::ng-deep {
.progress {
&.warning>progress {
color: orange;
&::-webkit-progress-value {
background-color: orange;
}
&::-moz-progress-bar {
background-color: orange;
}
}
}
top: 0.2rem;
right: -13.1rem;
}
.progress-label {
position: absolute;
right: -2.3rem;
top: 0;
width: 3.5rem;
right: -3rem;
margin-top: 0;
width: 4rem;
font-weight: 100;
font-size: 10px;
}
overflow: hidden;
text-overflow: ellipsis;
::ng-deep {
.clr-error {
.clr-validate-icon {
margin-left: -12px;
}
}
}
\ No newline at end of file
......@@ -38,10 +38,10 @@ export class EditProjectQuotasComponent implements OnInit {
staticBackdrop = true;
closable = false;
quotaForm: NgForm;
@ViewChild(InlineAlertComponent)
@ViewChild(InlineAlertComponent, {static: false})
inlineAlert: InlineAlertComponent;
@ViewChild('quotaForm')
@ViewChild('quotaForm', {static: true})
currentForm: NgForm;
@Output() confirmAction = new EventEmitter();
quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT;
......
......@@ -40,3 +40,19 @@
margin-top: auto;
cursor: pointer;
}
::ng-deep {
.progress {
&.warning>progress {
color: orange;
&::-webkit-progress-value {
background-color: orange;
}
&::-moz-progress-bar {
background-color: orange;
}
}
}
}
\ No newline at end of file
......@@ -33,7 +33,7 @@ const QuotaType = 'project';
export class ProjectQuotasComponent implements OnChanges {
config: Configuration = new Configuration();
@ViewChild('editProjectQuotas')
@ViewChild('editProjectQuotas', {static: false})
editQuotaDialog: EditProjectQuotasComponent;
loading = true;
quotaHardLimitValue: QuotaHardLimitInterface;
......
......@@ -29,10 +29,10 @@ export class RegistryConfigComponent implements OnInit {
@Input() hasAdminRole: boolean = false;
@ViewChild("systemSettings") systemSettings: SystemSettingsComponent;
@ViewChild("vulnerabilityConfig") vulnerabilityCfg: VulnerabilityConfigComponent;
@ViewChild("gc") gc: GcComponent;
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
@ViewChild("systemSettings", {static: false}) systemSettings: SystemSettingsComponent;
@ViewChild("vulnerabilityConfig", {static: false}) vulnerabilityCfg: VulnerabilityConfigComponent;
@ViewChild("gc", {static: false}) gc: GcComponent;
@ViewChild("cfgConfirmationDialog", {static: false}) confirmationDlg: ConfirmationDialogComponent;
constructor(
private configService: ConfigurationService,
......
......@@ -22,7 +22,7 @@ export class ReplicationConfigComponent {
@Input() showSubTitle: boolean = false;
@ViewChild("replicationConfigFrom") replicationConfigForm: NgForm;
@ViewChild("replicationConfigFrom", { static: false }) replicationConfigForm: NgForm;
get editable(): boolean {
return this.replicationConfig &&
......
<form #systemConfigFrom="ngForm" class="compact">
<section class="form-block">
<form #systemConfigFrom="ngForm" class="clr-form clr-form-horizontal">
<section>
<label class="subtitle" *ngIf="showSubTitle">{{'CONFIG.SYSTEM' | translate}}</label>
<div class="form-group">
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}</label>
<div class="select">
<select id="proCreation" name="proCreation"
[(ngModel)]="systemSettings.project_creation_restriction.value"
[disabled]="disabled(systemSettings.project_creation_restriction)">
<option value="everyone">{{'CONFIG.PRO_CREATION_EVERYONE' | translate }}</option>
<option value="adminonly">{{'CONFIG.PRO_CREATION_ADMIN' | translate }}</option>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.PRO_CREATION_RESTRICTION' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel"
[(ngModel)]="systemSettings.token_expiration.value"
required pattern="^[1-9]{1}[0-9]*$" id="tokenExpiration" size="20" [disabled]="!editable">
<span class="tooltip-content">
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
<clr-select-container>
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.TOOLTIP.PRO_CREATION_RESTRICTION' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="robotTokenExpiration" class="required">{{'ROBOT_ACCOUNT.TOKEN_EXPIRATION' | translate}}</label>
<label for="robotTokenExpiration" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="robotTokenExpirationInput.invalid && (robotTokenExpirationInput.dirty || robotTokenExpirationInput.touched)">
<input name="robotTokenExpiration" type="text" #robotTokenExpirationInput="ngModel"
(ngModelChange)="changeToken($event)" [(ngModel)]="robotTokenExpiration"
required pattern="^[1-9]{1}[0-9]*$" id="robotTokenExpiration" size="20"