Cleanup old add host

This commit is contained in:
Vincent Fiduccia 2017-06-03 01:20:39 -07:00
parent 4c0bcf6915
commit f811190e8a
No known key found for this signature in database
GPG Key ID: 2B29AD6BB2BB2582
19 changed files with 0 additions and 713 deletions

View File

@ -1,68 +0,0 @@
import Ember from 'ember';
const PROVIDERS = [
{
id: 'Amazon',
translationKey: 'hostTemplatesPage.keys.dropdownAdd.amazon'
},
{
id: 'Digital Ocean',
translationKey: 'hostTemplatesPage.keys.dropdownAdd.do'
},
{
id: 'Packet',
translationKey: 'hostTemplatesPage.keys.dropdownAdd.packet'
},
]
export default Ember.Controller.extend({
modalService: Ember.inject.service('modal'),
providers: PROVIDERS,
sortBy: 'flavorPrefix',
actions: {
newTemplateKey(id) {
this.get('modalService').toggleModal('modal-add-host-template', {
provider: id,
hostKeys: this.get('model'),
});
}
},
headers: [
{
name: 'state',
translationKey: 'hostTemplatesPage.table.state',
sort: ['state','flavorPrefix', 'name', 'created'],
width: '100'
},
{
name: 'flavorPrefix',
translationKey: 'hostTemplatesPage.table.flavor',
sort: ['flavorPrefix', 'state', 'name', 'created'],
width: '150'
},
{
name: 'name',
translationKey: 'hostTemplatesPage.table.name',
sort: ['name','flavorPrefix', 'state', 'created'],
width: ''
},
{
name: 'description',
translationKey: 'hostTemplatesPage.table.desc',
sort: ['state','flavorPrefix', 'name', 'created'],
width: ''
},
{
name: 'publicValues',
translationKey: 'hostTemplatesPage.table.public',
sort: ['publicValues','flavorPrefix', 'name', 'created'],
width: ''
},
{
name: 'created',
translationKey: 'hostTemplatesPage.table.created',
sort: ['created','flavorPrefix', 'name', 'state'],
width: '150'
},
],
});

View File

@ -1,9 +0,0 @@
import Ember from 'ember';
export default Ember.Route.extend({
model: function(/*params,transition*/) {
return this.get('store').find('hostTemplates', null, {forceReload: true}).then((templates) => {
return templates;
});
}
});

View File

@ -1,74 +0,0 @@
<section class="header clearfix">
<h1>{{t 'hostTemplatesPage.keys.header'}}</h1>
<div class="right-buttons">
<div class="template-add-key dropdown">
<a class="btn bg-primary dropdown-toggle icon-btn pl-10" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{t 'hostTemplatesPage.keys.buttonText'}}
<span class="sr-only">{{t 'nav.srToggleDropdown'}}</span>
<span class="darken"><i class="icon icon-chevron-down"></i></span>
</a>
<ul class="dropdown-menu dropdown-menu-right" data-dropdown-id="host-template-provider">
{{#each providers as |provider|}}
<li>
<button class="btn bg-transparent" {{action "newTemplateKey" provider.id}}>{{t provider.translationKey}}</button>
</li>
{{/each}}
</ul>
</div>
</div>
</section>
<section>
<p>{{t 'hostTemplatesPage.keys.header'}}</p>
{{#if instances.length}}
{{#sortable-table
classNames="grid fixed mt-10 sortable-table"
bulkActions=false
paging=false
search=false
sortBy=sortBy
headers=headers
body=model
fullRows=false
as |sortable kind key|
}}
{{#if (eq kind "row")}}
<td data-title="{{t 'hostTemplatesPage.table.state'}}:">
{{badge-state model=key}}
</td>
<td data-title="{{t 'hostTemplatesPage.table.flavor'}}:">
{{#if key.flavorPrefix}}{{key.flavorPrefix}}{{else}}<span class="text-muted">{{t 'hostTemplatesPage.table.noName'}}</span>{{/if}}
</td>
<td data-title="{{t 'hostTemplatesPage.table.name'}}:">
{{#if key.name}}{{key.displayName}}{{else}}<span class="text-muted">{{t 'hostTemplatesPage.table.noName'}}</span>{{/if}}
</td>
<td data-title="{{t 'hostTemplatesPage.table.desc'}}:">
{{#if key.description}}{{key.description}}{{else}}<span class="text-muted">{{t 'hostTemplatesPage.table.noDesc'}}</span>{{/if}}
</td>
<td data-title="{{t 'hostTemplatesPage.table.public'}}:">
{{#if key.publicValues}}
{{#copy-to-clipboard clipboardText=key.publicValues size="small"}}
{{key.publicValues}}
{{/copy-to-clipboard}}
{{else}}
<span class="text-muted">{{t 'hostTemplatesPage.table.noPublicValue'}}</span>
{{/if}}
</td>
<td data-title="{{t 'hostTemplatesPage.table.created'}}:">
{{date-calendar key.created}}
</td>
<td data-title="{{t 'generic.actions'}}:" class="actions">
{{action-menu model=key}}
</td>
{{else if (eq kind "norows")}}
<tr>
<td colspan="{{sortable.fullColspan}}" class="text-center text-muted pt-20 pb-20">{{t 'hostTemplatesPage.table.noData'}}</td>
</tr>
{{/if}}
{{/sortable-table}}
{{else}}
{{empty-table resource="container" showNew=false}}
{{/if}}
</section>

View File

@ -1,64 +0,0 @@
import Ember from 'ember';
// this component really doesn't care about the host provider
// all its going to do is validate and save so the partial could
// load the template for adding and saving keys
export default Ember.Component.extend({
store: Ember.inject.service(),
cloudPlans: Ember.inject.service(),
add: false,
templates: null,
hostTemplate: null,
selectedKey: null,
newSelectedKey: null,
provider: null,
providerKeyDetails: Ember.computed.alias('cloudPlans.hostDetails'),
name: null,
secretValue: null,
publicValue: null,
noSelect: false,
init() {
this._super(...arguments);
if (this.get('add') && this.get('selectedKey')) {
this.set('newSelectedKey', this.get('selectedKey'));
}
},
createNewTemplate: function() {
return Ember.$.extend(this.get('store').createRecord({type: 'hostTemplate'}), this.get('providerKeyDetails').findBy('flavorPrefix', this.get('provider')));
},
newKeyObserver: Ember.on('init', Ember.observer('name', 'secretValue', 'publicValue', function() {
var {name, secretValue, publicValue} = this.getProperties('name', 'secretValue', 'publicValue');
var selectedKey = this.get('selectedKey');
if (this.get('add')) {
this.set('selectedKey.name', name);
if (selectedKey.publicValues) {
Object.keys(selectedKey.publicValues).forEach((pvk) => {
selectedKey.publicValues[pvk] = publicValue;
});
}
if (selectedKey.secretValues) {
Object.keys(selectedKey.secretValues).forEach((svk) => {
selectedKey.secretValues[svk] = secretValue;
});
}
}
})),
actions: {
addKey() {
this.set('selectedKey', this.set('newSelectedKey', this.createNewTemplate()));
this.set('add', true);
},
cancelAdd(){
this.set('selectedKey', null);
this.set('add', false);
}
},
setHostTemplate: Ember.observer('hostTemplate', function() {
if (this.get('hostTemplate')) {
this.set('selectedKey', this.get('templates').findBy('id', this.get('hostTemplate')));
} else {
this.set('selectedKey', null);
}
}),
});

View File

@ -1,55 +0,0 @@
<div class="row">
{{#if add}}
<h3>{{t 'addHostTemplate.add.header'}}</h3>
{{#each-in newSelectedKey as |key value|}}
{{#if (eq key 'name')}}
<div class="col span-4">
<label class="pb-5" for="">{{key}}</label>
{{input type='text' value=name}}
</div>
{{/if}}
{{#if (eq key 'publicValues')}}
{{#each-in (get selectedKey key) as |pKey pValue|}}
<div class="col span-4">
<label class="pb-5" for="">{{pKey}}</label>
{{input type='text' value=publicValue}}
</div>
{{/each-in}}
{{/if}}
{{#if (eq key 'secretValues')}}
<div class="col span-4">
{{#each-in (get selectedKey key) as |sKey sValue|}}
<div>
<label class="pb-5" for="">{{sKey}}</label>
{{input type='password' value=secretValue}}
</div>
{{/each-in}}
{{#unless noSelect}}
<div class="col span-4">
<a class="pull-right" href="#" {{action 'cancelAdd'}}>{{t 'generic.cancel'}}</a>
</div>
{{/unless}}
</div>
{{/if}}
{{/each-in}}
{{else}}
<div class="col span-4">
<div class="col-inline clearfix">
<label class="pb-5" for="">{{t 'addHostTemplate.choose.label'}}</label>
<div class="pull-right text-small">
<a class="btn bg-transparent p-0" role="button" {{action "addKey"}}>{{t 'addHostTemplate.choose.add'}}</a>
</div>
</div>
{{new-select
classNames="form-control"
content=templates
optionLabelPath="name"
optionValuePath="id"
localizedLabel=false
localizedPrompt=true
prompt='addHostTemplate.choose.prompt'
value=hostTemplate
}}
</div>
{{/if}}
</div>

View File

@ -1,135 +0,0 @@
import Ember from 'ember';
const DEFAULT_REALM = 'us-west';
const PROVIDERS = [{id: 'All'}, {id: 'Amazon'}, {id: 'Digital Ocean' }, {id: 'Packet' }];
export default Ember.Component.extend({
prefs: Ember.inject.service(),
from: null,
tab: Ember.computed.alias('from'),
realmSort: DEFAULT_REALM,
providers: PROVIDERS,
memSort: null,
storageSort: null,
costSort: null,
providerSort: 'All',
sortBy: 'uiOptions.pricePerMonth',
actions: {
sendTab(id) {
this.get('triggerTabChange')(id);
},
selectMachine(id) {
this.setProperties({
providerSort: 'All',
memSort: null,
storageSort: null,
costSort: null,
realmSort: DEFAULT_REALM,
});
this.get('triggerSelectMachine')(id);
},
favoriteChanged(id) {
if (this.get('tab') === 'favorites') {
this.set('model.plans', this.get('model.plans').filter((item) => {
if (item.id !== id) {
return true;
}
return false;
}));
}
}
},
noDataMessage: Ember.computed('tab', function() {
if (this.get('tab') === 'favorites') {
return 'hostsPage.cloudHostsPage.browsePage.table.noFavs';
}
return 'hostsPage.cloudHostsPage.browsePage.table.noData';
}),
headers: [
{
name: 'uiOptions.favorite',
translationKey: 'hostsPage.cloudHostsPage.browsePage.table.favorite',
width: '100'
},
{
name: 'provider',
translationKey: 'hostsPage.cloudHostsPage.browsePage.table.provider',
sort: ['provider', 'uiOptions.pricePerMonth', 'uiOptions.id'],
width: '175'
},
{
name: 'uiOptions.zone',
translationKey: 'hostsPage.cloudHostsPage.browsePage.table.zone',
sort: ['uiOptions.zone', 'provider', 'uiOptions.id'],
},
{
name: 'uiOptions.displayName',
translationKey: 'hostsPage.cloudHostsPage.browsePage.table.instance',
sort: ['uiOptions.displayName', 'uiOptions.pricePerMonth', 'uiOptions.id'],
},
{
name: 'uiOptions.memory',
translationKey: 'hostsPage.cloudHostsPage.browsePage.table.memory',
sort: ['uiOptions.memory', 'uiOptions.pricePerMonth','uiOptions.displayName'],
},
{
name: 'uiOptions.transfer',
translationKey: 'hostsPage.cloudHostsPage.browsePage.table.transfer',
sort: ['uiOptions.transfer'],
},
// {
// name: 'cpuRating',
// translationKey: 'hostsPage.cloudHostsPage.browsePage.table.cpu',
// sort: ['cpuRating:desc','displayName'],
// width: 120,
// },
// {
// name: 'diskRating',
// translationKey: 'hostsPage.cloudHostsPage.browsePage.table.disk',
// sort: ['diskRating','displayName','zome'],
// width: 120
// },
{
name: 'uiOptions.pricePerMonth',
translationKey: 'hostsPage.cloudHostsPage.browsePage.table.price',
sort: ['uiOptions.pricePerMonth','uiOptions.memory:desc','uiOptions.displayName'],
width: 75,
},
{
width: 100,
},
],
providerContent: Ember.computed('model.plans', 'providerSort', function() {
var content = this.get('model.plans');
var prov = this.get('providerSort');
if (prov !== 'All') {
content = content.filterBy('provider', this.get('providerSort'))
}
return content;
}),
filteredContent: Ember.computed('providerContent', 'realmSort', 'costSort', 'storageSort', 'memSort', function() {
var rs = this.get('realmSort');
var cs = this.get('costSort');
var ms = this.get('memSort');
var ss = this.get('storageSort');
if (rs === 'all') {
return this.get('providerContent');
} else {
return this.get('providerContent').filter((plan) => {
return (
(!rs || plan.uiOptions.realm === rs) &&
(!ms || plan.uiOptions.memory >= ms) &&
(!ss || plan.uiOptions.storage >= ss) &&
(!cs || plan.uiOptions.pricePerMonth <= cs)
);
});
}
}),
});

View File

@ -1,23 +0,0 @@
<section class="header has-tabs clearfix hosts-cloud-new">
<div class="row">
<h1>{{t 'hostsPage.new.header.text'}}</h1>
{{#select-tab initialTab=from tab=tab tagName='div' classNames='inline-block' as |component|}}
<ul class="tab-nav" style="display: inline-block" role="tablist">
<li role="tab" aria-controls="panel" data-section="favorites" {{action "selectTab" "favorites" target=component}}>
<a href="#" {{action 'sendTab' 'favorites'}}>{{t 'hostsPage.cloudHostsPage.tabs.fav'}}</a>
</li>
<li role="tab" aria-controls="panel" data-section="browse" {{action "selectTab" "browse" target=component}}>
<a href="#" {{action 'sendTab' 'browse'}}>{{t 'hostsPage.cloudHostsPage.tabs.browse'}}</a>
</li>
</ul>
{{/select-tab}}
</div>
</section>
<section class="mt-20">
<div class="horizontal-form" role="tabpanel">
<div class="section" data-section="{{tab}}">
{{partial (concat 'host/cloud/' tab)}}
</div>
</div>
</section>

View File

@ -1,21 +0,0 @@
import Ember from 'ember';
import ModalBase from 'ui/mixins/modal-base';
import NewOrEdit from 'ui/mixins/new-or-edit';
export default Ember.Component.extend(ModalBase, NewOrEdit, {
classNames: ['medium-modal'],
store: Ember.inject.service(),
cloudPlans: Ember.inject.service(),
provider: Ember.computed.alias('modalOpts.provider'),
selectedHostTemplate: null,
primaryResource: Ember.computed.alias('selectedHostTemplate'),
hostDetails: Ember.computed.alias('cloudPlans.hostDetails'),
init() {
this._super(...arguments);
this.set('selectedHostTemplate', Ember.$.extend(this.get('store').createRecord({type: 'hostTemplate'}), this.get('hostDetails').findBy('flavorPrefix', this.get('provider'))));
},
doneSaving(neu) {
this.get('modalOpts.hostKeys.content').pushObject(neu);
this.send('cancel');
}
});

View File

@ -1,17 +0,0 @@
<div class="container-header-text">
<h3 class="alert-header">{{t 'addHostTemplate.add.header'}}</h3>
<hr/>
</div>
<div class="modal-body">
{{add-host-template
add=true
noSelect=true
templates=null
provider=provider
selectedKey=(mut selectedHostTemplate)
}}
</div>
{{top-errors errors=errors}}
<div class="footer-actions">
{{save-cancel save="save" cancel='cancel'}}
</div>

View File

@ -1,9 +0,0 @@
import Ember from 'ember';
export function hostPublicValues(params/*, hash*/) {
var [host] = params;
var pv = host.publicValues[`${host.driver}Config`];
return pv;
}
export default Ember.Helper.helper(hostPublicValues);

View File

@ -1,35 +0,0 @@
import Ember from 'ember';
export default Ember.Route.extend({
cloudPlans: Ember.inject.service(),
actions: {
save() {
this.transitionTo('hosts');
},
cancel() {
this.transitionTo('hosts.container-cloud');
}
},
model(params/*, transition*/){
if (params.cloud_id) {
return this.get('store').find('hostTemplates', null, {forceReload: true}).then((templates) => {
var plan = this.get('cloudPlans.plans').findBy('uiOptions.id', params.cloud_id);
return Ember.Object.create({
plans: plan,
hostTemplates: templates.filterBy('flavorPrefix', plan.pretty_provider),
host: this.get('store').createRecord({type: 'host'}),
});
});
} else {
return this.transitionTo('hosts.container-cloud');
}
},
resetController(controller, isExisting /*, transition*/) {
if ( isExisting )
{
controller.set('host', null);
controller.set('model', null);
}
},
});

View File

@ -1,7 +0,0 @@
{{cloud-host-add-or-edit
model=model.host
config=model.plans
hostTemplates=model.hostTemplates
cancel=(route-action 'cancel')
save=(route-action 'save')
}}

View File

@ -1,6 +0,0 @@
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['from'],
from: 'favorites',
});

View File

@ -1,55 +0,0 @@
import Ember from 'ember';
import C from 'ui/utils/constants';
export default Ember.Route.extend({
prefs: Ember.inject.service(),
cloudPlans: Ember.inject.service(),
queryParams: {
from: {
refreshModel: true
},
},
actions: {
selectMachine(id) {
this.transitionTo('hosts.container-cloud.add', id);
},
selectTab(from) {
this.transitionTo('hosts.container-cloud', {queryParams: {from: from}});
},
},
model(params/*, transition*/){
var model = {};
var plans = this.get('cloudPlans.plans');
model.realms = this.get('cloudPlans.realms');
switch(params.from) {
case 'favorites':
var favs = this.get(`prefs.${C.PREFS.HOST_FAVORITES}`) || [];
if (favs.length) {
model.plans = plans.filter((plan) => {
if (favs.includes(plan.uiOptions.id)) {
return true;
}
return false;
});
} else {
let defaults = ['digitalocean-sfo2-2gb', 'digitalocean-sfo2-4gb', 'digitalocean-sfo2-8gb', 'digitalocean-sfo2-16gb'];
model.plans = plans.filter((plan) => {
if (defaults.includes(plan.uiOptions.id)) {
return true;
}
});
this.set(`prefs.${C.PREFS.HOST_FAVORITES}`, model.plans.mapBy('uiOptions.id'));
}
break;
default:
case 'browse':
model.plans = plans;
break;
}
return model;
},
});

View File

@ -1 +0,0 @@
{{cloud-host-select from=from model=model triggerTabChange=(route-action 'selectTab') triggerSelectMachine=(route-action 'selectMachine')}}

View File

@ -1 +0,0 @@
{{outlet}}

View File

@ -1,50 +0,0 @@
<div class="row">
<div class="row">
<div class="col span-3">
<label for="">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.provider.label'}}</label>
{{new-select
classNames="form-control"
content=providers
optionLabelPath="id"
optionValuePath="id"
value=providerSort
}}
</div>
<div class="col span-3">
<label for="">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.realm.label'}}</label>
{{new-select
classNames="form-control"
content=model.realms
optionLabelPath="translationKey"
optionValuePath="id"
localizedLabel=true
value=realmSort
}}
</div>
</div>
<div class="row">
<div class="col span-3">
<label for="">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.mem.label'}}</label>
<div class="input-group">
{{input type='text' value=memSort}}
<span class="input-group-addon bg-default" type="button">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.mem.unit'}}</span>
</div>
</div>
<div class="col span-3">
<label for="">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.storage.label'}}</label>
<div class="input-group">
{{input type='text' value=storageSort}}
<span class="input-group-addon bg-default" type="button">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.storage.unit'}}</span>
</div>
</div>
<div class="col span-3">
<label for="">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.cost.label'}}</label>
<div class="input-group">
<span class="input-group-addon bg-default" type="button">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.cost.preUnit'}}</span>
{{input type='text' value=costSort}}
<span class="input-group-addon bg-default" type="button">{{t 'hostsPage.cloudHostsPage.browsePage.sorts.cost.unit'}}</span>
</div>
</div>
</div>
</div>
{{ partial 'host/cloud/table-content' }}

View File

@ -1,36 +0,0 @@
<section class="pl-15 pr-15 clearfix">
{{#each filteredContent as |host|}}
{{#catalog-box
model=host.uiOptions
active=(not (eq host.uiOptions.state 'inactive'))
classNames='cloud-host'
showIcon=false
showDescription=false
as |section|
}}
{{#if (eq section 'header')}}
{{mark-favorite classNames="pull-left" tagName="span" id=host.uiOptions.id rowRemoved=(action 'favoriteChanged') iconSize='icon-2x'}}
<div class="catalog-icon mb-10 mt-10 {{parse-host-icon host.provider}}"/>
{{else if (eq section 'body')}}
<hr class="m-10" />
<h2>{{host.uiOptions.displayName}}</h2>
<div>
<div class="details">
<div class="memory">
<strong>{{host.uiOptions.memory}}{{t 'hostsPage.cloudHostsPage.favoritesPage.memStUnit'}}</strong> <span class="help-text">{{t 'hostsPage.cloudHostsPage.favoritesPage.help.ram'}}</span>
</div>
<div class="storage">
<strong>{{host.uiOptions.storage}}{{#if (not (eq host.uiOptions.provider 'Amazon'))}}{{t 'hostsPage.cloudHostsPage.favoritesPage.memStUnit'}}{{/if}}</strong> {{#if (not (eq host.uiOptions.provider 'Amazon'))}}<span class="help-text">{{t 'hostsPage.cloudHostsPage.favoritesPage.help.storage'}}</span>{{/if}}
</div>
<div class="transfer">
<strong>{{host.uiOptions.transfer}}</strong>{{t 'hostsPage.cloudHostsPage.favoritesPage.transferUnit'}} <span class="help-text">{{t 'hostsPage.cloudHostsPage.favoritesPage.help.transfer'}}</span>
</div>
</div>
</div>
{{else if (eq section 'footer')}}
<button role="button" class="btn bg-primary" {{action "selectMachine" host.uiOptions.id}}>{{format-number host.uiOptions.pricePerMonth style='currency' currency='USD'}}{{t 'hostsPage.cloudHostsPage.favoritesPage.price'}}</button>
{{/if}}
{{/catalog-box}}
{{/each}}
</section>

View File

@ -1,47 +0,0 @@
<div class="row">
{{#sortable-table
classNames="grid sortable-table"
bulkActions=false
rowActions=false
paging=false
search=false
sortBy=sortBy
headers=headers
body=filteredContent
as |sortable kind row dt|
}}
{{#if (eq kind "row")}}
<td>
{{mark-favorite id=row.uiOptions.id rowRemoved=(action 'favoriteChanged')}}
</td>
<td>{{row.pretty_provider}}</td>
<td>{{row.uiOptions.zone}}</td>
<td>{{row.uiOptions.displayName}}</td>
<td>{{row.uiOptions.memory}} GB</td>
<td>
{{#if (eq row.uiOptions.transfer -1)}}
Unlimited
{{else if (eq row.uiOptions.transfer 0)}}
None
{{else}}
{{#if (eq row.provider 'amazonec2') }}
{{format-si row.uiOptions.transfer suffix="B" startingExponent=3}}
{{else}}
{{format-si row.uiOptions.transfer suffix="B" startingExponent=4}}
{{/if}}
{{/if}}
</td>
<!-- <td>{{star-rating rating=row.uiOptions.cpuRating}}</td> -->
<!-- <td>{{star-rating rating=row.uiOptions.diskRating}}</td> -->
<td>
{{format-number row.uiOptions.pricePerMonth style='currency' currency='USD' maximumFractionDigits=2}}{{#if row.uiOptions.variableFees}}<sup>&#10746;</sup>{{/if}}
</td>
<td class="text-right"><button role="button" class="btn bg-primary" {{action "selectMachine" row.uiOptions.id}}> {{t 'generic.select'}}</button></td>
{{else if (eq kind "norows")}}
<td colspan="{{sortable.fullColspan}}" class="text-center text-muted pt-20 pb-20">{{t noDataMessage}}</td>
{{/if}}
{{/sortable-table}}
<div class="pull-left text-small mt-10">{{#link-to "hosts.new"}}{{t 'hostsPage.cloudHostsPage.browsePage.table.classic'}}{{/link-to}}</div>
<div class="pull-right text-small mt-10">{{t 'hostsPage.cloudHostsPage.browsePage.table.help' htmlSafe=true}}</div>
</div>