added prices and fixed the title of all pages

This commit is contained in:
Dragos 2021-04-29 09:56:44 +03:00
parent 2b106c2741
commit aabc192b31
27 changed files with 443 additions and 63 deletions

View File

@ -32,7 +32,7 @@ export class AccountComponent implements OnInit, OnDestroy
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('account.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
translationService.get('account.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
//accountService.getUsers().subscribe(x => console.log(x));

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Cacheable } from 'ts-cacheable';
import { delay, filter, map, repeatWhen, take, tap } from 'rxjs/operators';
import { delay, filter, map, mergeMap, repeatWhen, take, tap } from 'rxjs/operators';
import { CatalogPackage } from '../models/package';
import { CatalogImage } from '../models/image';
@ -38,7 +38,16 @@ export class CatalogService
})
getPackages(): Observable<CatalogPackage[]>
{
return this.httpClient.get<CatalogPackage[]>(`/api/my/packages`);
return this.httpClient.get<CatalogPackage[]>(`/api/my/packages`)
.pipe(mergeMap(packages =>
{
return this.httpClient.get(`./assets/data/packages.json`).pipe(map(prices =>
{
packages.forEach(x => x.price = prices[x.id])
return packages;
}))
}));
}
// ----------------------------------------------------------------------------------------------------------------
@ -47,7 +56,16 @@ export class CatalogService
})
getPackage(packageId: string): Observable<CatalogPackage>
{
return this.httpClient.get<CatalogPackage>(`/api/my/packages/${packageId}`);
return this.httpClient.get<CatalogPackage>(`/api/my/packages/${packageId}`)
.pipe(mergeMap(pkg =>
{
return this.httpClient.get(`./assets/data/packages.json`).pipe(map(prices =>
{
pkg.price = prices[pkg.id];
return pkg;
}))
}));
}
// ----------------------------------------------------------------------------------------------------------------
@ -56,7 +74,16 @@ export class CatalogService
})
getImages(allStates = false): Observable<CatalogImage[]>
{
return this.httpClient.get<CatalogImage[]>(`/api/my/images?${allStates ? 'state=all' : ''}`);
return this.httpClient.get<CatalogImage[]>(`/api/my/images?${allStates ? 'state=all' : ''}`)
.pipe(mergeMap(images =>
{
return this.httpClient.get(`./assets/data/images.json`).pipe(map(prices =>
{
images.forEach(x => x.price = prices[x.id])
return images;
}))
}));
}
// ----------------------------------------------------------------------------------------------------------------
@ -65,7 +92,16 @@ export class CatalogService
})
getImage(id: string): Observable<CatalogImage>
{
return this.httpClient.get<CatalogImage>(`/api/my/images/${id}`);
return this.httpClient.get<CatalogImage>(`/api/my/images/${id}`)
.pipe(mergeMap(image =>
{
return this.httpClient.get(`./assets/data/images.json`).pipe(map(prices =>
{
image.price = prices[image.id];
return image;
}))
}));
}
// ----------------------------------------------------------------------------------------------------------------
@ -141,7 +177,7 @@ export class CatalogService
// ----------------------------------------------------------------------------------------------------------------
cloneImage(imageId: string): Observable<any>
{
// https://apidocs.joyent.com/cloudapi/#CloneImage
// https://apidocs.Spearhead.com/cloudapi/#CloneImage
return this.httpClient.post<any>(`/api/my/images/${imageId}?action=clone`, {})
.pipe(tap(() => imagesCacheBuster$.next()));
}

View File

@ -15,11 +15,11 @@
<div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom left">
<button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
Sort by
<b *ngIf="editorForm.get('sortProperty').value === 'name'">name</b>
<b *ngIf="editorForm.get('sortProperty').value === 'description'">description</b>
<b *ngIf="editorForm.get('sortProperty').value === 'os'">operating system</b>
<b *ngIf="editorForm.get('sortProperty').value === 'type'">type</b>
<b *ngIf="editorForm.get('sortProperty').value === 'state'">status</b>
<span *ngIf="editorForm.get('sortProperty').value === 'name'">name</span>
<span *ngIf="editorForm.get('sortProperty').value === 'description'">description</span>
<span *ngIf="editorForm.get('sortProperty').value === 'os'">operating system</span>
<span *ngIf="editorForm.get('sortProperty').value === 'type'">type</span>
<span *ngIf="editorForm.get('sortProperty').value === 'state'">status</span>
</button>
<ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem">
@ -62,7 +62,7 @@
<accordion [isAnimated]="false" [closeOthers]="false">
<accordion-group [isOpen]="myImagesExpanded">
<div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading
tooltip="Show or hide my images" placement="top" container="body">
tooltip="Show or hide my images" placement="top" container="body" [adaptivePosition]="false">
<h4 class="text-info text-uppercase mb-0">My images</h4>
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="myImagesExpanded ? 90 : 0" class="text-info"></fa-icon>

View File

@ -42,7 +42,7 @@ export class ImagesComponent implements OnInit, OnDestroy
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('catalog.images.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
translationService.get('catalog.images.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
// Configure FuseJs
this.fuseJsOptions = {

View File

@ -14,4 +14,5 @@ export class CatalogImage
requirements: any;
homepage: string;
image_size: number;
price: number;
}

View File

@ -17,4 +17,5 @@ export class CatalogPackage
description: string;
disks: any[];
flexible_disk: boolean;
price: number;
}

View File

@ -22,6 +22,7 @@
<span class="d-block">
<span class="h3 text-uppercase">
{{ pkg.name }}
<span class="price" *ngIf="pkg.price">{{ pkg.price | currency }}/h</span>
<!--<small *ngIf="pkg.brand">{{ pkg.brand }}</small>-->
</span>
<small class="text-faded pb-1 d-block">v<b>{{ pkg.version }}</b></small>

View File

@ -137,7 +137,7 @@ export class PackagesComponent implements OnInit, OnDestroy, OnChanges
p.visible = this.image.requirements.brand === p.brand;
if (this.image.type === 'zone-dataset')
p.visible = ['joyent', 'joyent-minimal'].includes(p.brand);
p.visible = ['Spearhead', 'Spearhead-minimal'].includes(p.brand);
if (this.image.type === 'lx-dataset')
p.visible = p.brand === 'lx';

View File

@ -208,6 +208,20 @@ export class InstancesService
{
return this.httpClient.get(`/api/my/machines/${instanceId}/audit`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable()
getImagesRates(): Observable<any>
{
return this.httpClient.get(`./assets/data/images.json`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable()
getPackagesRates(): Observable<any>
{
return this.httpClient.get(`./assets/data/packages.json`);
}
}
export type InstanceCallbackFunction = ((instance: Instance) => void);

View File

@ -35,7 +35,7 @@ export class MigrationsService
// ----------------------------------------------------------------------------------------------------------------
migrate(instanceId: string): Observable<any>
{
// https://apidocs.joyent.com/cloudapi/#Migrate
// https://apidocs.Spearhead.com/cloudapi/#Migrate
return this.httpClient.post(`/api/my/machines/${instanceId}/migrate`, { action: 'begin | sync | switch | automatic | pause | abort | watch', affinity: [] })
.pipe(tap(() => cacheBuster$.next()));
}
@ -43,7 +43,7 @@ export class MigrationsService
// ----------------------------------------------------------------------------------------------------------------
getMigrationProgress(instanceId: string): Observable<any>
{
// https://apidocs.joyent.com/cloudapi/#Migrate
// https://apidocs.Spearhead.com/cloudapi/#Migrate
return this.httpClient.get(`/api/my/machines/${instanceId}/migrate?action=watch`);
}

View File

@ -61,7 +61,10 @@
<span class="badge rounded-pill" [class.bg-success]="image.state === 'active'">&nbsp;</span>
</small>
<span class="d-block">
<span class="h3">{{ image.name }}</span>
<span class="h3">
{{ image.name }}
<span class="price" *ngIf="image.price">{{ image.price | currency }}</span>
</span>
<small class="text-faded pb-1 d-block">v<b>{{ image.version }}</b></small>
</span>
<span class="small">
@ -248,7 +251,7 @@
</div>
</div>
<div class="px-4" *ngIf="currentStep === 4">
<div class="px-4 d-flex flex-column h-100" *ngIf="currentStep === 4">
<!--<h5>Tell us which <b>data center</b> we should place this machine in</h5>
<select class="form-select image-type-selector" aria-label="Choose data center" formControlName="dataCenter">
<option [value]="dataCenter" *ngFor="let dataCenter of dataCenters">{{ dataCenter | uppercase }}</option>
@ -303,25 +306,32 @@
</div>
</div>
<p class="mt-3 lead text-success">
You're about to create
<b *ngIf="editorForm.get('imageType').value == 1">an infrastructure container</b>
<b *ngIf="editorForm.get('imageType').value == 2">a virtual machine</b>
<!--<b *ngIf="editorForm.get('imageType').value == 3">a Docker container</b>-->,
<div class="my-3">
<h5>Get an <b>estimated montly cost</b> based on the machine's running time</h5>
<b *ngIf="editorForm.get('package').value.description">{{ editorForm.get('package').value.description }}</b>
<div class="input-group input-group-cost">
<input type="number" min="1" max="1000000" class="form-control text-center" formControlName="estimatedMinutesRan"
placeholder="Number of hours"
tooltip="Number of hours this machine is running" />
<span class="input-group-text" tooltip="Package hourly rate">
hours
<b class="px-1">× {{ editorForm.get('package').value.price | currency: 'USD': 'symbol': '1.2-4' }}</b>
<span class="d-none d-sm-inline text-lowercase">(package hourly rate)</span>
</span>
<span class="input-group-text" *ngIf="editorForm.get('image').value.price" tooltip="Image monthly price">
<b class="px-1">+ {{ editorForm.get('image').value.price | currency: 'USD': 'symbol': '1.2-4' }}</b>
<span class="d-none d-sm-inline ps-1 text-lowercase">(Image monthly price)</span>
</span>
<span class="input-group-text" tooltip="Estimated cost per month">
<b>= {{ estimatedCost | currency: 'USD': 'symbol': '1.2-4' }}</b>
<span class="d-none d-sm-inline ps-1">(per month)</span>
</span>
</div>
</div>
<span *ngIf="!editorForm.get('package').value.description">
having
<b>{{ editorForm.get('package').value.vcpus || 1 }} vCPUs</b>,
<b>{{ editorForm.get('package').value.memory*1024*1024 | fileSize }} RAM</b> and
<b>{{ editorForm.get('package').value.disk*1024*1024 | fileSize }} storage</b>
</span>,
<span class="flex-grow-1"></span>
named <b>{{ editorForm.get('name').value }}</b>,
based on the <b>{{ editorForm.get('image').value.description }}</b>
</p>
<p class="my-3 lead text-success" [innerHtml]="readyText"></p>
</div>
</div>

View File

@ -130,7 +130,7 @@ p
float: none;
width: 1.4em;
max-width: 1rem;
margin-bottom: .25rem;
margin-bottom: .75rem;
cursor: inherit;
background-color: #0dc3e9;
border-color: #0dc3e9;
@ -213,6 +213,7 @@ p
a
{
color: #0dc3e9;
margin-left: .25rem;
}
}
}
@ -337,3 +338,24 @@ p.lead b
}
}
}
.input-group-cost
{
.form-control
{
border-top-left-radius: .25rem;
border-bottom-left-radius: .25rem;
}
.input-group-text
{
background: transparent;
color: #3d5e8e;
border-color: #3d5e8e;
b
{
color: #ff9c07;
}
}
}

View File

@ -12,6 +12,7 @@ import { NetworkingService } from '../../networking/helpers/networking.service';
import { ToastrService } from 'ngx-toastr';
import { VolumesService } from '../../volumes/helpers/volumes.service';
import { AuthService } from '../../helpers/auth.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-instance-wizard',
@ -49,6 +50,8 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
steps: any[];
preselectedPackage: string;
kvmRequired: boolean;
estimatedCost: number;
readyText: string;
private destroy$ = new Subject();
private userId: string;
@ -63,7 +66,8 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
private readonly catalogService: CatalogService,
private readonly networkingService: NetworkingService,
private readonly volumesService: VolumesService,
private readonly toastr: ToastrService)
private readonly toastr: ToastrService,
private readonly translateService: TranslateService)
{
// When the user navigates away from this route, hide the modal
router.events
@ -134,7 +138,8 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
}),
dataCenter: [],
tags,
metadata
metadata,
estimatedMinutesRan: [24]
});
this.configureForm();
@ -257,6 +262,8 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
this.kvmRequired = x?.requirements['brand'] === 'kvm' || x?.type === 'zvol' || false;
this.loadingPackages = true;
this.computeEstimatedCost();
});
this.editorForm.get('package').valueChanges
@ -269,6 +276,8 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
this.kvmRequired = this.editorForm.get('image').value?.requirements['brand'] === 'kvm' ||
this.editorForm.get('image').value?.type === 'zvol' ||
x?.brand === 'kvm' || false;
this.computeEstimatedCost();
});
this.editorForm.get(['affinity', 'farFrom']).valueChanges.pipe(startWith(null))
@ -278,6 +287,20 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
this.editorForm.get(['affinity', 'closeTo']).valueChanges.pipe(startWith(null))
.pipe(takeUntil(this.destroy$))
.subscribe(this.setAffinity.bind(this));
this.editorForm.get('estimatedMinutesRan').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(this.computeEstimatedCost.bind(this))
}
// ----------------------------------------------------------------------------------------------------------------
private computeEstimatedCost()
{
const estimatedMinutesRan = this.editorForm.get('estimatedMinutesRan').value || 1;
const imagePrice = this.editorForm.get('image').value?.price || 0;
const packagePrice = this.editorForm.get('package').value?.price || 0;
this.estimatedCost = imagePrice + packagePrice * estimatedMinutesRan;
}
// ----------------------------------------------------------------------------------------------------------------
@ -373,6 +396,20 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
nextStep()
{
this.currentStep = this.currentStep < this.steps.length ? this.currentStep + 1 : this.steps.length;
if (this.currentStep < this.steps.length) return;
this.readyText = this.translateService.instant('dashboard.wizard.ready', {
imageType: this.editorForm.get('imageType').value == 1
? this.translateService.instant('dashboard.wizard.readyImageTypeContainer')
: this.translateService.instant('dashboard.wizard.readyImageTypeVm'),
packageDescription: this.editorForm.get('package').value.description ||
`<b>${this.editorForm.get('package').value.vcpus || 1}</b> vCPUs, ` +
`<b>${this.fileSizePipe.transform(this.editorForm.get('package').value.memory * 1024 * 1024)}</b> RAM, ` +
`<b>${this.fileSizePipe.transform(this.editorForm.get('package').value.disk * 1024 * 1024)}</b> storage`,
machineName: this.editorForm.get('name').value,
imageDescription: this.editorForm.get('image').value.description
})
}
// ----------------------------------------------------------------------------------------------------------------

View File

@ -83,8 +83,8 @@
</div>
</div>
<div class="overflow-auto flex-grow-1 mt-3" id="scrollingBlock">
<div class="container py-2">
<div class="overflow-auto flex-grow-1 mt-3 d-flex flex-column" id="scrollingBlock">
<div class="container flex-grow-1 py-2">
<h2 *ngIf="listItems && listItems.length === 0 && instances && instances.length > 0" class="text-uppercase">
{{ 'dashboard.list.noResults' | translate }}
</h2>
@ -159,19 +159,13 @@
</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"
<fa-icon icon="power-off" [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"
<button class="btn btn-link text-info" [popover]="instanceContextMenu" container="body"
[popoverContext]="{ instance: instance }" placement="bottom left" containerClass="menu-dropdown"
[outsideClick]="true">
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>

View File

@ -1,6 +1,6 @@
:host
{
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}

View File

@ -78,7 +78,7 @@ export class InstancesComponent implements OnInit, OnDestroy
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('dashboard.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
translationService.get('dashboard.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
this.lazyLoadDelay = this.minimumLazyLoadDelay;

View File

@ -21,9 +21,9 @@
<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 *ngIf="editorForm.get('sortProperty').value === 'action'">action</b>
<b *ngIf="editorForm.get('sortProperty').value === 'enabled'">status</b>
<b *ngIf="editorForm.get('sortProperty').value === 'description'">description</b>
<span *ngIf="editorForm.get('sortProperty').value === 'action'">action</span>
<span *ngIf="editorForm.get('sortProperty').value === 'enabled'">status</span>
<span *ngIf="editorForm.get('sortProperty').value === 'description'">description</span>
</button>
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem">

View File

@ -39,7 +39,7 @@ export class FirewallRulesComponent implements OnInit, OnDestroy
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('networking.firewall.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
translationService.get('networking.firewall.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
// Configure FuseJs
this.fuseJsOptions = {

View File

@ -20,7 +20,7 @@
<div class="btn-group flex-grow-1 flex-grow-sm-0 mb-3 w-sm-auto w-100" dropdown placement="bottom left">
<button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
Sort by <b>{{ editorForm.get('sortProperty').value === 'vlan_id' ? 'id' : editorForm.get('sortProperty').value }}</b>
Sort by {{ editorForm.get('sortProperty').value === 'vlan_id' ? 'id' : editorForm.get('sortProperty').value }}
</button>
<ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem">

View File

@ -38,7 +38,7 @@ export class NetworksComponent implements OnInit, OnDestroy
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('networking.networks.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
translationService.get('networking.networks.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
this.getVlans();

View File

@ -39,7 +39,7 @@ export class SecurityComponent implements OnInit, OnDestroy
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('security.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
translationService.get('security.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
forkJoin({
users: securityService.getUsers(),

View File

@ -20,7 +20,7 @@
<div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom left">
<button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
Sort by <b>{{ editorForm.get('sortProperty').value === 'vlan_id' ? 'id' : editorForm.get('sortProperty').value }}</b>
Sort by {{ editorForm.get('sortProperty').value === 'vlan_id' ? 'id' : editorForm.get('sortProperty').value }}
</button>
<ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem">

View File

@ -43,7 +43,7 @@ export class VolumesComponent implements OnInit, OnDestroy
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('volumes.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
translationService.get('volumes.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
// Configure FuseJs
this.fuseJsOptions = {

View File

@ -0,0 +1,6 @@
{
"9d73e502-9fa6-11e3-7da8-bb6a8876bb21": 70,
"8d73e502-9fa6-11e3-7da8-bb6a8876bb21": 70,
"6d73e502-9fa6-11e3-7da8-bb6a8876bb21": 70,
"3d73e502-9fa6-11e3-8fa8-bb6a8876bb21": 70
}

View File

@ -0,0 +1,248 @@
{
"22218452-a2d5-ea3c-d7db-8ac394f396b7": 0.066,
"f10e7447-c35f-4e0f-9e0b-f7d22366341e": 0.027,
"8b4fdd0b-6448-692e-8b47-924d9b6fa298": 0.089,
"47fe0a45-c839-eecf-df1a-ffbb835ceb79": 0.034,
"468c03e2-334c-cd99-a8cd-c24e220f18c8": 0.296,
"0b95abef-a7b4-4875-db2b-d7711f4ea31f": 0.186,
"372b15e7-2a4f-4e1a-b46d-97f795623ec7": 0.178,
"127a3a30-d7a3-c31b-cac3-b76c79afca71": 0.186,
"372271df-c3b6-e9a1-9308-e2e91ea7a83b": 0.027,
"a6df1682-d50b-c357-9c11-de94d5893c44": 0.044,
"5af898f4-34c9-c9bc-c232-aef873f37c07": 0.089,
"bd32d8e3-e5b7-6e8e-91f1-8eadc2e3fbd9": 0.355,
"182a5990-a524-cd82-eb2e-90aa1b092abc": 0.044,
"948d12c0-3270-c097-9d0c-8efb9f9353eb": 0.034,
"db0093fc-916f-682b-c1c1-b69275ed45bf": 0.298,
"7cca3866-dad8-615e-b349-b28c632b625a": 0.298,
"c3c0de9d-91a6-46d0-f750-b3da8437bd74": 0.235,
"88e0f747-29fc-6787-9c11-ac0914bd7527": 0.178,
"600737b8-081a-ceed-8ff9-ffae9feba617": 0.044,
"cedb1804-54df-4bb2-e069-cdd4cc684704": 0.044,
"8d88e28a-ef4f-ef3a-a006-917c252e3c70": 0.178,
"70bc395a-3d8b-4833-81d1-af4be39af974": 0.089,
"52068036-7178-4645-b988-be84f5195332": 0.355,
"26a95af3-dd19-6df2-d22c-a14c8d485cd8": 0.004,
"8ed7a214-dfa9-6462-923a-ff67279cfa66": 0.002,
"5dd22dfb-e595-4f36-bada-f690b8166376": 0.006,
"0580e086-d4a6-442c-c7a7-d674f2245301": 0.008,
"8e93c22f-d513-6fd8-d440-ffcaf0cd4589": 0.012,
"6de53561-6e14-6a8a-bc94-a1566b5725a9": 0.014,
"3e64d126-0a6e-c4e4-9d84-81243ea43493": 0.016,
"c47d5124-63cd-6ceb-dbba-b6f42464b147": 0.041,
"c37a77ba-f0cd-42fa-feb6-9e8dff956d87": 0.062,
"4faddb31-1c71-ef29-bcd8-b7910ea3e6fe": 0.021,
"ed4d095d-f18c-443a-c847-ad64c4af44fa": 0.018,
"393e32ae-79f7-6b97-c43c-e5e7614cacf4": 0.082,
"1b2cc5f5-5ce4-62c8-f552-f3b8dec2caea": 0.010,
"fb25ac7c-78ee-6cd1-ede4-dd7e1c703a89": 0.103,
"bf25f174-41d3-eae5-b158-980dcdd99051": 0.123,
"0737f0ad-4303-ed1d-d400-88cc21698cd8": 0.164,
"683e437e-1f9c-c713-caed-fd2a89e15ac5": 0.185,
"4406105d-ff34-438e-f097-d8f3c1a3fc49": 0.144,
"963b791d-b101-4021-bf51-fed400088ab3": 0.205,
"9031e7d1-2965-65e2-9c57-801930bdf67f": 0.030,
"123407e6-c995-6ece-b2cf-b391e653385c": 0.060,
"8238843b-5daf-c98f-8c65-c09f064370d9": 0.040,
"edd4d2b9-ee55-ebbb-aff4-ef0a61917bed": 0.080,
"22b44234-b13b-47a7-f7fa-dcb68cccc3e5": 0.160,
"03ce4af4-4d24-c4b6-f31e-ad2e9fede8f6": 0.268,
"0fcc3289-8cdf-ce8c-83ec-ab275fec9183": 0.276,
"a188646b-530d-cd90-cd22-cf2f42a93351": 0.276,
"a93f6fd2-ca7b-e15c-96e8-cc1c3b61fce8": 0.283,
"c88e3006-6f04-66d6-a742-ad6b43e1ccd2": 0.283,
"3e64bdda-ef4e-4c9a-c481-e15c0d40c9b9": 0.030,
"53213fe1-3d60-6ed6-9904-ed8c32aab4e6": 0.040,
"d9a46a90-9816-6a74-abed-f67a198f646e": 0.080,
"fe4bcadd-eb50-e1c9-e712-fb0cd724aefe": 0.160,
"34e48ec3-3872-c4e6-c17c-eb626145833e": 0.268,
"14b4c0d9-32ab-e23c-a03a-b7420829c66c": 0.276,
"232ef917-2e0a-68d0-9238-b825e08e640f": 0.060,
"b03c27ca-bf76-e6e1-8404-e486b0aee197": 0.117,
"2950e56a-33af-ca43-b19a-ea165f64632e": 0.241,
"cca75627-5669-4fd6-8849-df2bf3fe3891": 0.121,
"ff3348da-dc29-40c8-a5b5-c3f73198e27e": 0.024,
"28804ada-03e5-ef21-94a2-bf1af50ed71f": 0.093,
"9c20bda4-aeb2-ed87-e90f-e965ea6bb06a": 0.320,
"64ceedda-b153-e18b-bcd6-8c604fd7aa8e": 0.076,
"85b4bd34-19fa-6d7b-f957-bc041f74f465": 1.682,
"f8130ce9-6f0a-cdb6-9d15-d6975a0db5d2": 0.118,
"380a57a4-7b12-cf8f-b376-eef360d3d544": 0.235,
"3f9f4d22-e762-cb15-db7b-b9a354ae4ccd": 0.026,
"402b7496-63f3-e60a-d852-96e3f1abc976": 0.053,
"d577dcb8-f7a5-672c-ae2a-b83141659f83": 0.106,
"b7a97f19-4b0a-4704-fcf0-a3796a2c04fe": 0.211,
"b31fc9e7-59ae-46ec-bd37-9038abffe940": 0.422,
"5d04037d-313d-cc39-e780-baafeb352376": 0.855,
"c387fb70-f95a-4026-f470-ef8a6ec170c9": 0.080,
"0702404f-94e7-666e-cc33-ec0d602d13ea": 0.150,
"b76821ed-d842-6965-a960-b576cb239153": 0.300,
"082aa0f1-5cae-6789-98e2-cacacbf0c13d": 0.370,
"28001852-d8c0-4697-b36c-fe3d41b092b4": 0.422,
"50cdae86-3136-ead9-b3e1-fe9b369fa43c": 0.034,
"7f69ed8b-2c9e-6757-bae1-9732710fc965": 0.159,
"69e46054-1961-487a-9e6f-a37ebdfe2048": 0.283,
"06959c2d-2390-ce94-9a6b-fc38537e632e": 0.024,
"aa7976f9-ee28-e16e-8a91-f0bd601dd0d1": 0.076,
"03b52642-7572-cbb4-89b1-80bd1b93e2e4": 0.060,
"ad22bdf1-a3c4-c6b8-b5b9-e2dc5d67b1cd": 0.060,
"dd153024-c7fe-44b0-929c-ea3e3e16e1ae": 0.117,
"063843ba-dc3b-6a09-8630-cf34c11245e2": 0.040,
"d4341f46-c9e6-e4cc-e9f3-b9fb3f9c8adb": 0.380,
"e90e0436-f01b-c3a2-bad3-92e37659c34b": 0.370,
"74ca19ac-741e-cd9f-fba7-dd87f8ad7caa": 0.370,
"71eb8b96-1792-c639-94e7-924f76014bf6": 0.053,
"7e1ac676-b3df-c80e-cf10-d7550caccd07": 0.106,
"e516512d-1539-6dc5-e1b5-9d6acb812279": 0.211,
"7f588535-1e16-e77a-dde0-b133031f96c1": 0.855,
"430ae3ee-a8c6-e92d-feb7-aebe6d86d897": 0.080,
"19e12410-1f04-e822-e828-999d7a7a4140": 0.150,
"8847f7dc-a195-66a2-a316-c273e9622f04": 0.300,
"1951fdbe-b753-e80f-89b1-aca3bffdc20d": 0.380,
"6e47f0c5-8d86-6ae1-b9e9-85d9d60177f8": 0.283,
"034ede05-5bc4-cc9d-cebe-9e013e9b8fa2": 0.159,
"eb662801-1f41-6b49-96d0-f19baf370978": 0.040,
"e22534bb-bbee-e834-a158-c46a4debe1cb": 0.080,
"80930ddd-cb9d-6cf6-d4fd-ddb6c9b3dd99": 0.160,
"1d08bf5d-c5a3-cf4f-ab6b-a1c224415cf5": 0.268,
"a9fb2452-5674-efee-be80-b07f1e1fa713": 0.276,
"d75a45f1-5063-4b6b-d4b1-d02c3fb342e7": 0.121,
"7e6f2e3e-f644-47a3-b751-fd94f3f1acec": 0.211,
"9047dfd5-0c2e-6b2e-8e37-d41d1687b7a5": 0.320,
"39207fcc-338c-e0e5-ad8e-f528ce4fc684": 0.093,
"cca6a0a8-5685-cd79-de28-ac7bda8427bc": 0.283,
"87828074-22c5-6e4e-a2de-9783bf424eb2": 0.118,
"a22daa6c-6973-ecba-c354-eb618808f8f6": 0.241,
"42b27154-3478-ce5d-8425-f026fb6b52f7": 1.682,
"8f799461-8139-cc23-9b26-9b049d2c62f8": 0.106,
"8b9cf667-aac9-69a0-c3ea-fcb7a4824a0d": 0.106,
"fb92e427-a4b9-4cae-dfa7-dc71bcd41863": 0.076,
"85293756-d2b1-6c50-d79c-ea0d9bcfc7e5": 0.079,
"287c3d76-deec-4aed-9407-c0bb73e6ba4a": 0.134,
"c62c0364-b47c-c723-b48a-a98299d3133d": 0.268,
"52e42908-74e9-e702-a1ef-a54e92587f92": 0.390,
"a585f939-ea86-6a4d-a28e-b083f08a8cea": 0.280,
"67764556-cc01-c47d-c994-99e1fd03b768": 0.241,
"c76cd2f7-6034-e117-f796-d54364002ecf": 0.022,
"d2170614-2c80-cdc8-853f-e92bbd09e568": 0.022,
"0b5f4762-9666-eeba-d3d7-95eecc815db7": 0.022,
"e131e417-f7e3-48bf-91b4-ea12d5df6b94": 0.038,
"9a6c17ba-02f4-c187-b16a-9e9c8cfbf575": 0.030,
"d60377d2-3e4f-ecf0-c013-b9231e814df4": 0.049,
"059da774-2e9f-eeb7-eaea-a25275e534f4": 0.380,
"0c592d4a-5bd6-e2d3-c58c-885ee024c88f": 0.607,
"8cf5eca4-a00e-e826-b803-a38398bb0a81": 0.607,
"2c0eb6a0-896b-c60c-ce38-cbae9aa3705f": 0.243,
"05859b98-ecfe-cd38-a06e-ac5bf224031f": 0.133,
"e8079c97-09ce-ea1c-8d58-ea864d22d836": 0.017,
"b80a7cd8-4699-ed26-dbe6-c9c804f03d9c": 0.268,
"7fb90259-f044-c432-bf27-f5008963e37e": 0.117,
"f5bee04c-208c-66e5-927c-86ebed05fe4e": 0.117,
"f3cfc57b-5aa8-ebdc-c4ff-ea1ea9ccbab4": 0.106,
"3bead424-e188-60e8-d435-82ebec698c4d": 0.053,
"125117ae-5d01-ca19-a9db-bd55fcae6149": 0.268,
"a1b71c42-f9ca-6874-d6eb-b10053bf17c1": 0.106,
"e97fd9a8-afe3-4b95-e7c1-b6fc186b615c": 0.080,
"5e62be81-ae92-efc4-c42a-c6954a23de78": 0.117,
"aaabc1cb-9857-4f7a-8893-ff179dde23c6": 0.243,
"f4979b69-4fd0-6e1a-db4c-ff4de8170071": 0.076,
"66b73715-85ed-cb64-b5a1-bab78eb20e15": 0.160,
"b744d6d8-82f4-6a6c-bc10-a8fc2034ef0d": 0.205,
"d7544477-53b3-e8eb-e70f-eb3142bc4e19": 0.034,
"af1bce69-09ed-6ba4-c3a6-d2f14a3ad493": 0.026,
"5597cead-e17a-e675-9a4b-e412610cad07": 0.205,
"99fc25e2-605c-c1a7-f490-a445aef18795": 0.060,
"113ebd76-81b3-ed7d-f35f-f713bdb72249": 0.241,
"78283ad0-7c5e-6a76-d46b-b5dad5d06338": 0.211,
"1e95adcb-e160-c773-e375-b57933d0aae0": 0.040,
"a40afe3b-67be-64fa-df7e-aadda730e283": 0.855,
"6c01f552-6cd4-687d-b7f4-81ae67cbde18": 0.133,
"406815fc-dd97-48da-904d-c7bd311aba76": 0.283,
"d799e656-b36d-ef42-f1ee-cfef82af082f": 0.283,
"8688a1b3-9242-c57c-a845-aa3d4119bb5b": 0.320,
"5a134b97-60d7-ea74-8913-c52f4c94e4b5": 0.012,
"62e20132-3f0d-eb82-ecbd-cf90c04093ce": 0.013,
"8b15210a-da5f-4964-b851-d618cff5c573": 0.027,
"63c21b52-935a-4e05-f646-9d373cc3a133": 0.028,
"20f10e49-027a-c0bb-932a-f1eec8ca113f": 0.042,
"34301e2d-bcf5-4dad-b796-c2d3a3cdbe47": 0.043,
"12f9fee9-5784-c680-da49-b4ada33f1d9b": 0.017,
"70f9800c-a6a3-c9c5-c849-8ebf193ddeea": 0.018,
"5e9d3545-69c0-ca03-f18b-c99c45c9791f": 0.029,
"17888b77-0366-6ae5-8639-a5eec5fef1b9": 0.030,
"a7c106f6-547d-cc81-95a1-b42c61c5816d": 0.044,
"421b3879-dd3c-6898-fa54-a720ff473999": 0.045,
"4a2b3050-ac7f-4b4a-b31c-c64e7cf2dcfc": 0.034,
"4840c833-6512-4fb7-fcf3-d72dbf29dbe2": 0.036,
"a7bca2ec-1117-6655-8dc7-8f41f3d50b64": 0.039,
"539774b0-a675-48f2-fd0d-8315e0542693": 0.040,
"7bdee0ba-8645-cbfc-9445-b95921ebcd62": 0.054,
"c0ef4b70-5de1-c3b3-ef3a-f54d831c3784": 0.055,
"ef90a622-ca4b-ef21-8ba7-e8121b3ee67a": 0.085,
"0a045323-e248-6e15-8d1e-f7444dae67e9": 0.086,
"bf2a90a7-8c9d-6c6d-e64c-c2544cbe04a7": 0.059,
"6366217f-de24-6582-b579-87bfa760ce82": 0.061,
"ed2a66de-e6f2-c271-cd3e-efdbca767414": 0.089,
"5102fab9-de80-67ea-ef04-c705196db078": 0.091,
"094a37cd-30a4-6d7f-cc50-e4fc847c8f73": 0.149,
"21dbb603-b3cf-4b45-9daf-9540266c28d9": 0.151,
"de83c846-d1ff-c3db-8b00-b4f9ebb80f65": 0.270,
"c28d671a-d97e-eefc-90c3-da266d2feb03": 0.272,
"9d7ef1b3-827f-4450-ed67-c33eb23c475f": 0.067,
"7e248a9a-1733-e896-e807-c477cd7c238c": 0.071,
"8ab362c8-845a-cca8-cc92-cfa4ab63edfa": 0.097,
"d61d0546-34a3-c830-d417-9d6585e0330e": 0.101,
"c67ffac8-a5b9-cc1b-d7b3-8af7972c877d": 0.157,
"34c08664-8df3-68ea-af28-941c23b90f18": 0.162,
"e5946d15-cf2b-4c6d-f873-c99429791c62": 0.084,
"771a1b42-ee5a-c1c8-cf05-ec1585008c1e": 0.092,
"bbef2ee6-d72a-cc18-b20e-ec8b67bbb313": 0.114,
"0916e773-ee6e-4e19-eef9-80f481758a32": 0.122,
"0ba70a68-2c1f-657d-e7f4-a2930a996dfe": 0.174,
"cc8a39a7-3c24-4da7-e899-b3214081ce97": 0.183,
"172e9c01-80e6-ecae-de2a-e7228cbfa144": 0.295,
"95449608-bf37-cbcc-e2eb-d60f2ac1db35": 0.303,
"166bebac-fd73-e468-fe2d-e8b699375053": 0.042,
"cf102146-317d-e11b-e16c-f9187cc3b00b": 0.053,
"ae407ea8-8461-41e3-a472-f75d2d522416": 0.117,
"eccca454-7282-e301-a195-8254ecde71ad": 0.121,
"10678275-7ed1-66d1-b3a9-eaf6683e90b9": 0.178,
"0835d49c-1457-614e-d313-bf1453dc3cff": 0.182,
"653d6003-d480-e1b7-a5a3-ccd543831f39": 0.298,
"47e9a13d-b26f-ea6b-d612-c9f027387076": 0.302,
"2e4fda7b-f26f-c154-8269-de574d471aba": 0.419,
"30c668ce-60b3-e16c-c5aa-8639aebaf204": 0.423,
"a52bf2a8-30a5-438c-b12c-86c409f57d8e": 0.134,
"95c502a0-f3a6-690a-d879-e818fb5459c3": 0.142,
"1a7bde87-21c2-e8d2-c377-909561a5c012": 0.194,
"44070f9d-a679-e7a1-a6cc-829b28353331": 0.203,
"6045d5d1-b32d-6f99-bfbd-aa9b9b10f75f": 0.315,
"e8f0e856-34d1-efca-b4d5-fd77122e779c": 0.323,
"23944cb0-9ad9-60aa-f030-b83fee428a0f": 0.436,
"e176dbf4-2b1b-e2cf-cd67-faf4294f0c3d": 0.444,
"ce0d9d18-4ed9-e6fb-8a0e-ddd9c809a69d": 0.469,
"39595bee-160e-6138-b7f3-9b115f68b843": 0.486,
"96b20a93-a579-e27b-fbdb-e37f83e7334e": 0.094,
"782b2dcc-0d37-cb1f-e84b-ee07558e9af1": 0.188,
"f724f364-6e29-4333-d486-9b262317c0a4": 0.192,
"299def39-e8cd-67b5-bedd-896e10e6dede": 0.235,
"53d7d9d5-b32e-614f-dcf7-9ab961195d48": 0.243,
"a36a2eb4-00a2-6a53-8077-83e69d4275f7": 0.355,
"f4a527f7-04a0-61af-e076-b118055acaaf": 0.363,
"6bf8b9dd-538c-ce57-8acc-9dda062bab3c": 0.476,
"d0457e57-85f6-656e-f06f-cb642d149934": 0.484,
"910f7fe4-c6f4-c880-fca5-d8c9a879a33f": 0.268,
"8a83258f-fbe1-caf7-c66f-f3ee258cb8a0": 0.285,
"2c0aeb79-14e4-c554-ff83-9becfbd22549": 0.389,
"ba3ffdf0-c362-60f0-96b1-c16a7be36c51": 0.405,
"23ed3a18-5664-4349-f927-c9d67fd3c4a5": 0.509,
"e06b4d1b-14d3-4d44-fca8-d61449a8830d": 0.526,
"ae681b38-58c1-6047-e038-9aefa021f7de": 0.288,
"9f2ec40f-2ae6-441f-b1a3-9f3ae518b912": 0.305,
"de01c282-b7e2-45a3-c789-87102bf13cde": 0.469,
"69f98564-9094-4582-ba4d-ae90ce769325": 0.486,
"f35153c9-d979-6a17-f66e-82c3532f76ef": 0.576,
"4a3c6572-84ba-4149-d619-e02002a082ae": 0.610,
"93602faa-c78c-e104-fb0b-f7e802f4bd97": 0.992,
"1efc247e-0213-e7de-97de-daa5767dd3cf": 1.059
}

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Joyent</title>
<title>Spearhead</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@ -541,3 +541,13 @@ accordion
{
text-transform: uppercase;
}
.price
{
color: #cd5c5c;
vertical-align: middle;
padding: 0 .5rem;
margin-bottom: .25rem;
display: inline-block;
text-transform: none;
}