340 lines
18 KiB
HTML
340 lines
18 KiB
HTML
|
<div class="d-flex flex-column h-100" [formGroup]="editorForm">
|
||
|
<div class="container text-center mt-1">
|
||
|
<div class="btn-toolbar pt-2">
|
||
|
<div class="btn-group flex-grow-1 flex-grow-sm-0">
|
||
|
<button class="btn btn-lg btn-info" (click)="createMachine()" [disabled]="loadingIndicator">
|
||
|
Create a new machine
|
||
|
</button>
|
||
|
</div>
|
||
|
|
||
|
<span class="d-none d-sm-block flex-grow-1"></span>
|
||
|
|
||
|
<ng-container *ngIf="instances && instances.length">
|
||
|
<div class="input-group input-group-pill flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
|
||
|
<input type="text" class="form-control" placeholder="Search..."
|
||
|
formControlName="searchTerm" appAlphaOnly="^[A-Za-z0-9_-]+$"
|
||
|
tooltip="Search by name, tag, metadata, operating system or brand" placement="top" container="body" [adaptivePosition]="false">
|
||
|
<button class="btn btn-outline-info" type="button" (click)="clearSearch()" [disabled]="!editorForm.get('searchTerm').value"
|
||
|
tooltip="Clear search" container="body" placement="top" [adaptivePosition]="false">
|
||
|
<fa-icon icon="times" size="sm" [fixedWidth]="true"></fa-icon>
|
||
|
</button>
|
||
|
</div>
|
||
|
|
||
|
<div class="btn-group flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
|
||
|
<button class="btn btn-outline-info dropdown-toggle" [disabled]="loadingIndicator"
|
||
|
[popover]="filtersTemplate" [outsideClick]="true" container="body" placement="bottom right" containerClass="menu-popover">
|
||
|
Showing {{ listItems.length }} / {{ instances.length }}
|
||
|
<ng-container *ngIf="runningInstanceCount && stoppedInstanceCount">
|
||
|
<span class="badge rounded-pill bg-success text-dark">{{ runningInstanceCount }} running</span>
|
||
|
<span class="badge rounded-pill bg-danger text-dark ms-1">{{ stoppedInstanceCount }} stopped</span>
|
||
|
</ng-container>
|
||
|
<ng-container *ngIf="runningInstanceCount && !stoppedInstanceCount">
|
||
|
<span class="badge rounded-pill bg-success text-dark">{{ runningInstanceCount }} running</span>
|
||
|
</ng-container>
|
||
|
<ng-container *ngIf="!runningInstanceCount && stoppedInstanceCount">
|
||
|
<span class="badge rounded-pill bg-danger text-dark">{{ stoppedInstanceCount }} stopped</span>
|
||
|
</ng-container>
|
||
|
</button>
|
||
|
</div>
|
||
|
|
||
|
<div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom right">
|
||
|
<button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
|
||
|
Sort by <b>{{ editorForm.get('sortProperty').value }}</b>
|
||
|
</button>
|
||
|
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'name'" (click)="setSortProperty('name')">
|
||
|
Name
|
||
|
</button>
|
||
|
</li>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'os'" (click)="setSortProperty('os')">
|
||
|
Operating system
|
||
|
</button>
|
||
|
</li>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'brand'" (click)="setSortProperty('brand')">
|
||
|
Brand
|
||
|
</button>
|
||
|
</li>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'image'" (click)="setSortProperty('image')">
|
||
|
Image
|
||
|
</button>
|
||
|
</li>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'state'" (click)="setSortProperty('state')">
|
||
|
State
|
||
|
</button>
|
||
|
</li>
|
||
|
</ul>
|
||
|
</div>
|
||
|
</ng-container>
|
||
|
</div>
|
||
|
|
||
|
<div class="spinner-border text-center text-info text-faded" role="status" *ngIf="loadingIndicator">
|
||
|
<span class="visually-hidden">Loading...</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="overflow-auto my-2" id="scrollingBlock">
|
||
|
<div class="container">
|
||
|
<h2 *ngIf="listItems && listItems.length === 0 && instances && instances.length > 0" class="text-uppercase">
|
||
|
No machine matches your filters
|
||
|
</h2>
|
||
|
|
||
|
<virtual-scroller #scroller [items]="listItems" bufferAmount="2" class="instances"
|
||
|
[parentScroll]="scroller.window.document.getElementById('scrollingBlock')" [scrollThrottlingTime]="250">
|
||
|
<div *ngFor="let instance of scroller.viewPortItems; trackBy: trackByFunction; let index = index"
|
||
|
[ngClass]="showMachineDetails ? 'col-lg-6 col-12 full-details' : 'col-xl-2 col-lg-3 col-md-4 col-sm-6 col-12'"
|
||
|
lazyLoad [lazyLoadDelay]="lazyLoadDelay" [container]="scroller.element.nativeElement.getElementsByClassName('scrollable-content')[0]"
|
||
|
(canLoad)="instance.loading = false" (unload)="instance.loading = true" (load)="loadInstanceDetails(instance)">
|
||
|
<fieldset class="card" [disabled]="instance.working">
|
||
|
<div class="row g-0">
|
||
|
<div class="card-info" [ngClass]="showMachineDetails ? 'col-lg-4' : 'col'">
|
||
|
<div>
|
||
|
<h5 class="card-title text-truncate" [tooltip]="instance.name" container="body" placement="top" [adaptivePosition]="false">
|
||
|
{{ instance.name }}
|
||
|
</h5>
|
||
|
|
||
|
<div *ngIf="!instance.loading && instance.imageDetails" class="text-truncate small text-info text-faded mb-1"
|
||
|
[tooltip]="instance.imageDetails.description" container="body" placement="top" [adaptivePosition]="false">
|
||
|
{{ instance.imageDetails.name }}, v{{ instance.imageDetails.version }}
|
||
|
</div>
|
||
|
|
||
|
<button *ngIf="!instance.loading" class="btn btn-outline-info w-100 d-flex justify-content-around align-items-center text-truncate"
|
||
|
tooltip="Change specifications" container="body" placement="top" [adaptivePosition]="false"
|
||
|
(click)="resizeMachine(instance)" [disabled]="instance.brand === 'kvm'">
|
||
|
|
||
|
<!--<span class="text-uppercase text-truncate">{{ instance.packageDetails.name }}</span>-->
|
||
|
<span class="px-1">
|
||
|
<fa-icon icon="microchip"></fa-icon>
|
||
|
{{ instance.memory * 1024 * 1024 | fileSize }}
|
||
|
</span>
|
||
|
<span>
|
||
|
<fa-icon icon="server"></fa-icon>
|
||
|
|
||
|
{{ instance.disk * 1024 * 1024 | fileSize }}
|
||
|
</span>
|
||
|
</button>
|
||
|
</div>
|
||
|
|
||
|
<div class="text-center" *ngIf="instance.working">
|
||
|
<div class="spinner-border spinner-border-sm text-info text-faded" role="status">
|
||
|
<span class="visually-hidden">Working...</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div>
|
||
|
<div class="small text-truncate my-2">
|
||
|
<ng-container *ngIf="instance.type === 'virtualmachine'">
|
||
|
<fa-icon icon="server" size="sm"></fa-icon>
|
||
|
<b class="text-uppercase ms-1">{{ instance.brand }}</b> - infrastructure container
|
||
|
</ng-container>
|
||
|
<ng-container *ngIf="instance.type === 'smartmachine'">
|
||
|
<fa-icon icon="desktop" size="sm"></fa-icon>
|
||
|
<b class="text-uppercase ms-1">{{ instance.brand }}</b> - virtual machine
|
||
|
</ng-container>
|
||
|
</div>
|
||
|
|
||
|
<div class="d-flex flex-nowrap justify-content-between align-items-center">
|
||
|
<a href="javascript:void(0)" class="badge text-uppercase"
|
||
|
[class.bg-light]="instance.state !== 'running' && instance.state !== 'stopped'"
|
||
|
[class.bg-danger]="instance.state === 'stopped'"
|
||
|
[class.bg-success]="instance.state === 'running'"
|
||
|
(click)="showMachineHistory(instance)" tooltip="Show machine history" container="body" placement="top" [adaptivePosition]="false">
|
||
|
{{ instance.state }}
|
||
|
</a>
|
||
|
|
||
|
<div class="btn-group btn-group-sm" dropdown placement="bottom right" *ngIf="!instance.loading">
|
||
|
<button class="btn btn-link text-warning" (click)="restartMachine(instance)" *ngIf="instance.state === 'running'">
|
||
|
<fa-icon icon="power-off" [fixedWidth]="true" size="sm" tooltip="Restart this machine" container="body" placement="top" [adaptivePosition]="false"></fa-icon>
|
||
|
</button>
|
||
|
|
||
|
<button class="btn btn-link text-success" (click)="startMachine(instance)" *ngIf="instance.state === 'stopped'">
|
||
|
<fa-icon icon="play" [fixedWidth]="true" size="sm" tooltip="Start this machine" container="body" placement="top" [adaptivePosition]="false"></fa-icon>
|
||
|
</button>
|
||
|
|
||
|
<button class="btn btn-link text-info"
|
||
|
[popover]="instanceContextMenu" [popoverContext]="{ instance: instance }"
|
||
|
placement="bottom left" containerClass="menu-dropdown" [outsideClick]="true">
|
||
|
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="col mt-sm-0 mt-3 no-overflow-sm" *ngIf="showMachineDetails">
|
||
|
<div class="card-header p-0 h-100">
|
||
|
<tabset class="dashboard-tabs" *ngIf="!instance.loading">
|
||
|
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-info">
|
||
|
<ng-template tabHeading>
|
||
|
<fa-icon icon="info-circle" class="d-sm-none"></fa-icon>
|
||
|
<span class="d-none d-sm-inline-block ms-1">Info</span>
|
||
|
</ng-template>
|
||
|
<div class="card-body p-2 h-100">
|
||
|
<app-instance-info [instance]="instance" [loadInfo]="true"
|
||
|
(beforeLoad)="instance.working = true" (load)="instance.working = false">
|
||
|
</app-instance-info>
|
||
|
</div>
|
||
|
</tab>
|
||
|
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-networks">
|
||
|
<ng-template tabHeading>
|
||
|
<fa-icon icon="network-wired" class="d-sm-none"></fa-icon>
|
||
|
<span class="d-none d-sm-inline-block ms-1">Network</span>
|
||
|
</ng-template>
|
||
|
<div class="card-body p-2 h-100">
|
||
|
<app-instance-networks [instance]="instance" [loadNetworks]="instance.shouldLoadNetworks"
|
||
|
(beforeLoad)="instance.working = true" (load)="instance.working = false"
|
||
|
(instanceReboot)="watchInstanceState(instance)" (instanceStateUpdate)="updateInstance(instance, $event)">
|
||
|
</app-instance-networks>
|
||
|
</div>
|
||
|
</tab>
|
||
|
<!--<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-firewall">
|
||
|
<ng-template tabHeading>
|
||
|
<fa-icon icon="fire-alt" class="d-sm-none"></fa-icon>
|
||
|
<span class="d-none d-sm-inline-block ms-1">Firewall</span>
|
||
|
</ng-template>
|
||
|
<div class="card-body p-2 h-100">
|
||
|
<app-instance-firewall-rules [instance]="instance" [loadFirewallRules]="instance.shouldLoadFirewallRules"
|
||
|
(cloudFirewallChange)="instance.firewall_enabled = $event">
|
||
|
</app-instance-firewall-rules>
|
||
|
</div>
|
||
|
</tab>-->
|
||
|
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-volumes"
|
||
|
*ngIf="instance.volumes && instance.volumes.length">
|
||
|
<ng-template tabHeading>
|
||
|
<fa-icon icon="database" class="d-sm-none"></fa-icon>
|
||
|
<span class="d-none d-sm-inline-block ms-1">Volumes</span>
|
||
|
</ng-template>
|
||
|
<div class="card-body p-2 h-100">
|
||
|
<ul class="list-group list-group-flush list-info">
|
||
|
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
|
||
|
*ngFor="let volume of instance.volumes">
|
||
|
<div class="text-truncate">
|
||
|
<fa-icon icon="database" [fixedWidth]="true" size="sm"></fa-icon>
|
||
|
<span class="ms-1">
|
||
|
{{ volume.name }}
|
||
|
</span>
|
||
|
|
||
|
{{ volume.size * 1024 * 1024 | fileSize }}
|
||
|
</div>
|
||
|
</li>
|
||
|
</ul>
|
||
|
</div>
|
||
|
</tab>
|
||
|
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-snapshots"
|
||
|
*ngIf="instance.brand !== 'kvm'">
|
||
|
<ng-template tabHeading>
|
||
|
<fa-icon icon="history" class="d-sm-none"></fa-icon>
|
||
|
<span class="d-none d-sm-inline-block ms-1">Snapshots</span>
|
||
|
</ng-template>
|
||
|
<div class="card-body p-2 h-100">
|
||
|
<app-instance-snapshots [instance]="instance" [loadSnapshots]="instance.shouldLoadSnapshots"
|
||
|
(beforeLoad)="instance.working = true" (load)="instance.working = false"
|
||
|
(instanceStateUpdate)="updateInstance(instance, $event)">
|
||
|
</app-instance-snapshots>
|
||
|
</div>
|
||
|
</tab>
|
||
|
<tab *ngIf="false" customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-migrations">
|
||
|
<ng-template tabHeading>
|
||
|
<fa-icon icon="coins" class="d-sm-none"></fa-icon>
|
||
|
<span class="d-none d-sm-inline-block ms-1">Migrations</span>
|
||
|
</ng-template>
|
||
|
<div class="card-body p-2 h-100">
|
||
|
<button class="btn btn-outline-info w-100">Move to another node</button>
|
||
|
</div>
|
||
|
</tab>
|
||
|
</tabset>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</fieldset>
|
||
|
</div>
|
||
|
</virtual-scroller>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<ng-template #filtersTemplate [formGroup]="editorForm">
|
||
|
<fieldset class="filters">
|
||
|
<ng-container formGroupName="filters">
|
||
|
<div class="dropdown-header">Filter by state</div>
|
||
|
<select class="form-control mb-3" formControlName="stateFilter">
|
||
|
<option [ngValue]="null">Any state</option>
|
||
|
<option *ngFor="let state of instanceStateArray" [value]="state">{{ state }}</option>
|
||
|
</select>
|
||
|
|
||
|
<div class="dropdown-header">Filter by memory</div>
|
||
|
<ngx-slider class="mb-4" formControlName="memoryFilter" [options]="memoryFilterOptions"></ngx-slider>
|
||
|
|
||
|
<div class="dropdown-header">Filter by disk size</div>
|
||
|
<ngx-slider class="mb-3" formControlName="diskFilter" [options]="diskFilterOptions"></ngx-slider>
|
||
|
|
||
|
<button class="btn btn-outline-dark w-100 mt-3" (click)="clearFilters()">Reset filters</button>
|
||
|
</ng-container>
|
||
|
|
||
|
<div class="dropdown-divider"></div>
|
||
|
<div class="form-check form-switch">
|
||
|
<input class="form-check-input mt-0" type="checkbox" id="showMachineDetails" formControlName="showMachineDetails">
|
||
|
<label class="form-check-label" for="showMachineDetails">
|
||
|
Show machine details
|
||
|
<fa-icon icon="spinner" [pulse]="true" size="sm" class="me-1" *ngIf="editorForm.get('showMachineDetails').disabled"></fa-icon>
|
||
|
</label>
|
||
|
</div>
|
||
|
</fieldset>
|
||
|
</ng-template>
|
||
|
|
||
|
<ng-template #instanceContextMenu let-instance="instance">
|
||
|
<ul class="list-group list-group-flush" role="menu">
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" (click)="renameMachine(instance)">
|
||
|
<fa-icon icon="pen" [fixedWidth]="true"></fa-icon>
|
||
|
Rename this machine
|
||
|
</button>
|
||
|
</li>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" (click)="showTagEditor(instance)">
|
||
|
<fa-icon icon="tags" [fixedWidth]="true"></fa-icon>
|
||
|
Edit machine tags
|
||
|
</button>
|
||
|
</li>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" (click)="showTagEditor(instance, true)">
|
||
|
<fa-icon icon="tags" [fixedWidth]="true"></fa-icon>
|
||
|
Edit machine metadata
|
||
|
</button>
|
||
|
</li>
|
||
|
<li class="dropdown-divider"></li>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" (click)="createMachine(instance)">
|
||
|
<fa-icon icon="clone" [fixedWidth]="true"></fa-icon>
|
||
|
Clone this machine
|
||
|
</button>
|
||
|
</li>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" (click)="createImageFromMachine(instance)">
|
||
|
<fa-icon icon="layer-group" [fixedWidth]="true"></fa-icon>
|
||
|
Create an image from this machine
|
||
|
</button>
|
||
|
</li>
|
||
|
<li class="dropdown-divider"></li>
|
||
|
<ng-container *ngIf="instance.state === 'running'">
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" (click)="stopMachine(instance)">
|
||
|
<fa-icon icon="stop" [fixedWidth]="true"></fa-icon>
|
||
|
Stop this machine
|
||
|
</button>
|
||
|
</li>
|
||
|
<li class="dropdown-divider"></li>
|
||
|
</ng-container>
|
||
|
<li role="menuitem">
|
||
|
<button class="dropdown-item" (click)="deleteMachine(instance)">
|
||
|
<fa-icon icon="trash" [fixedWidth]="true"></fa-icon>
|
||
|
Delete this machine
|
||
|
</button>
|
||
|
</li>
|
||
|
</ul>
|
||
|
</ng-template>
|