Merge pull request #12082 from hashicorp/f-ui/refactor-namespace

namespace refactoring
This commit is contained in:
Jai
2022-02-17 11:04:36 -05:00
committed by GitHub
30 changed files with 803 additions and 415 deletions

View File

@@ -12,8 +12,7 @@
<li>
<LinkTo
@route="jobs.job.index"
@model={{trigger.data.result.plainId}}
@query={{hash namespace=(or trigger.data.result.namespace.name "default")}}
@model={{trigger.data.result}}
data-test-breadcrumb={{"jobs.job.index"}}
>
<dl>
@@ -30,8 +29,7 @@
<li>
<LinkTo
@route="jobs.job.index"
@model={{this.job.plainId}}
@query={{hash namespace=(or this.job.namespace.name "default")}}
@model={{this.job}}
data-test-breadcrumb={{"jobs.job.index"}}
data-test-job-breadcrumb
>

View File

@@ -104,9 +104,12 @@ export default class JobDispatch extends Component {
const dispatch = yield this.args.job.dispatch(paramValues, this.payload);
// Navigate to the newly created instance.
this.router.transitionTo('jobs.job', dispatch.DispatchedJobID, {
queryParams: { namespace: this.args.job.get('namespace.name') },
});
const namespaceId = this.args.job.belongsTo('namespace').id();
const jobId = namespaceId
? `${dispatch.DispatchedJobID}@${namespaceId}`
: dispatch.DispatchedJobID;
this.router.transitionTo('jobs.job', jobId);
} catch (err) {
const error = messageFromAdapterError(err) || 'Could not dispatch job';
this.errors.pushObject(error);

View File

@@ -24,7 +24,6 @@ export default class JobClientStatusSummary extends Component {
this.router.transitionTo('jobs.job.clients', this.job, {
queryParams: {
status: JSON.stringify(statusFilter),
namespace: this.job.get('namespace.name'),
},
});
}

View File

@@ -26,8 +26,6 @@ export default class JobRow extends Component {
@action
gotoJob() {
const { job } = this;
this.router.transitionTo('jobs.job', job.plainId, {
queryParams: { namespace: job.get('namespace.name') },
});
this.router.transitionTo('jobs.job.index', job.idWithNamespace);
}
}

View File

@@ -37,9 +37,8 @@ export default class AllocationsAllocationController extends Controller {
label: allocation.taskGroupName,
args: [
'jobs.job.task-group',
job.plainId,
job.idWithNamespace,
allocation.taskGroupName,
jobQueryParams,
],
},
{

View File

@@ -114,9 +114,10 @@ export default class IndexController extends Controller.extend(
gotoVolume(volume, event) {
lazyClick([
() =>
this.transitionToRoute('csi.volumes.volume', volume.get('plainId'), {
queryParams: { volumeNamespace: volume.get('namespace.name') },
}),
this.transitionToRoute(
'csi.volumes.volume',
volume.get('idWithNamespace')
),
event,
]);
}

View File

@@ -5,7 +5,6 @@ import Controller from '@ember/controller';
import { action, computed, get } from '@ember/object';
import { scheduleOnce } from '@ember/runloop';
import intersection from 'lodash.intersection';
import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
import Sortable from 'nomad-ui/mixins/sortable';
import Searchable from 'nomad-ui/mixins/searchable';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
@@ -174,12 +173,7 @@ export default class TaskGroupController extends Controller.extend(
return {
title: 'Task Group',
label: name,
args: [
'jobs.job.task-group',
job,
name,
qpBuilder({ jobNamespace: job.get('namespace.name') || 'default' }),
],
args: ['jobs.job.task-group', job, name],
};
}
}

View File

@@ -35,6 +35,17 @@ export default class Job extends Model {
@attr() periodicDetails;
@attr() parameterizedDetails;
@computed('plainId')
get idWithNamespace() {
const namespaceId = this.belongsTo('namespace').id();
if (!namespaceId || namespaceId === 'default') {
return this.plainId;
} else {
return `${this.plainId}@${namespaceId}`;
}
}
@computed('periodic', 'parameterized', 'dispatched')
get hasChildren() {
return this.periodic || (this.parameterized && !this.dispatched);

View File

@@ -40,6 +40,13 @@ export default class Volume extends Model {
@attr('number') controllersHealthy;
@attr('number') controllersExpected;
@computed('plainId')
get idWithNamespace() {
// does this handle default namespace -- I think the backend handles this for us
// but the client would always need to recreate that logic
return `${this.plainId}@${this.belongsTo('namespace').id()}`;
}
@computed('controllersHealthy', 'controllersExpected')
get controllersHealthyProportion() {
return this.controllersHealthy / this.controllersExpected;

View File

@@ -21,13 +21,17 @@ export default class VolumeRoute extends Route.extend(WithWatchers) {
}
serialize(model) {
return { volume_name: model.get('plainId') };
return { volume_name: JSON.parse(model.get('id')).join('@') };
}
model(params, transition) {
const namespace = transition.to.queryParams.namespace;
const name = params.volume_name;
model(params) {
// Issue with naming collissions
const url = params.volume_name.split('@');
const namespace = url.pop();
const name = url.join('');
const fullId = JSON.stringify([`csi/${name}`, namespace || 'default']);
return RSVP.hash({
volume: this.store.findRecord('volume', fullId, { reload: true }),
namespaces: this.store.findAll('namespace'),

View File

@@ -11,12 +11,12 @@ export default class JobRoute extends Route {
@service token;
serialize(model) {
return { job_name: model.get('plainId') };
return { job_name: model.get('idWithNamespace') };
}
model(params, transition) {
const namespace = transition.to.queryParams.namespace || 'default';
const name = params.job_name;
model(params) {
const [name, namespace = 'default'] = params.job_name.split('@');
const fullId = JSON.stringify([name, namespace]);
return this.store

View File

@@ -5,11 +5,19 @@
<div data-test-inline-error class="notification is-danger">
<div class="columns">
<div class="column">
<h3 data-test-inline-error-title class="title is-4">{{this.error.title}}</h3>
<h3
data-test-inline-error-title
class="title is-4"
>{{this.error.title}}</h3>
<p data-test-inline-error-body>{{this.error.description}}</p>
</div>
<div class="column is-centered is-minimum">
<button data-test-inline-error-close class="button is-danger" onclick={{action this.onDismiss}} type="button">Okay</button>
<button
data-test-inline-error-close
class="button is-danger"
onclick={{action this.onDismiss}}
type="button"
>Okay</button>
</div>
</div>
</div>
@@ -17,13 +25,19 @@
<h1 data-test-title class="title with-headroom with-flex">
<div>
Allocation {{this.model.name}}
<span class="bumper-left tag {{this.model.statusClass}}">{{this.model.clientStatus}}</span>
Allocation
{{this.model.name}}
<span
class="bumper-left tag {{this.model.statusClass}}"
>{{this.model.clientStatus}}</span>
</div>
<div>
{{#if this.model.isRunning}}
<div class="two-step-button">
<Exec::OpenButton @job={{this.model.job}} @allocation={{this.model}} />
<Exec::OpenButton
@job={{this.model.job}}
@allocation={{this.model}}
/>
</div>
<TwoStepButton
data-test-stop
@@ -33,8 +47,12 @@
@confirmText="Yes, Stop"
@confirmationMessage="Are you sure? This will reschedule the allocation on a different client."
@awaitingConfirmation={{this.stopAllocation.isRunning}}
@disabled={{or this.stopAllocation.isRunning this.restartAllocation.isRunning}}
@onConfirm={{perform this.stopAllocation}} />
@disabled={{or
this.stopAllocation.isRunning
this.restartAllocation.isRunning
}}
@onConfirm={{perform this.stopAllocation}}
/>
<TwoStepButton
data-test-restart
@alignRight={{true}}
@@ -43,8 +61,12 @@
@confirmText="Yes, Restart"
@confirmationMessage="Are you sure? This will restart the allocation in-place."
@awaitingConfirmation={{this.restartAllocation.isRunning}}
@disabled={{or this.stopAllocation.isRunning this.restartAllocation.isRunning}}
@onConfirm={{perform this.restartAllocation}} />
@disabled={{or
this.stopAllocation.isRunning
this.restartAllocation.isRunning
}}
@onConfirm={{perform this.restartAllocation}}
/>
{{/if}}
</div>
</h1>
@@ -55,13 +77,24 @@
</span>
<div class="boxed-section is-small">
<div data-test-allocation-details class="boxed-section-body inline-definitions">
<div
data-test-allocation-details
class="boxed-section-body inline-definitions"
>
<span class="label">Allocation Details</span>
<span class="pair job-link"><span class="term">Job</span>
<LinkTo @route="jobs.job" @model={{this.model.job}} @query={{hash jobNamespace=this.model.job.namespace.id}} data-test-job-link>{{this.model.job.name}}</LinkTo>
<LinkTo
@route="jobs.job"
@model={{this.model.job}}
data-test-job-link
>{{this.model.job.name}}</LinkTo>
</span>
<span class="pair node-link"><span class="term">Client</span>
<LinkTo @route="clients.client" @model={{this.model.node}} data-test-client-link>{{this.model.node.shortId}}</LinkTo>
<LinkTo
@route="clients.client"
@model={{this.model.node}}
data-test-client-link
>{{this.model.node.shortId}}</LinkTo>
</span>
</div>
</div>
@@ -74,16 +107,26 @@
{{#if this.model.isRunning}}
<div class="columns">
<div class="column">
<PrimaryMetric::Allocation @allocation={{this.model}} @metric="cpu" />
<PrimaryMetric::Allocation
@allocation={{this.model}}
@metric="cpu"
/>
</div>
<div class="column">
<PrimaryMetric::Allocation @allocation={{this.model}} @metric="memory" />
<PrimaryMetric::Allocation
@allocation={{this.model}}
@metric="memory"
/>
</div>
</div>
{{else}}
<div data-test-resource-error class="empty-message">
<h3 data-test-resource-error-headline class="empty-message-headline">Allocation isn't running</h3>
<p class="empty-message-body">Only running allocations utilize resources.</p>
<h3
data-test-resource-error-headline
class="empty-message-headline"
>Allocation isn't running</h3>
<p class="empty-message-body">Only running allocations utilize
resources.</p>
</div>
{{/if}}
</div>
@@ -95,13 +138,17 @@
<div class="boxed-section-head">
Tasks
</div>
<div class="boxed-section-body {{if this.sortedStates.length "is-full-bleed"}}">
<div
class="boxed-section-body {{if this.sortedStates.length 'is-full-bleed'}}"
>
{{#if this.sortedStates.length}}
<ListTable
@source={{this.sortedStates}}
@sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}}
@class="is-striped" as |t|>
@class="is-striped"
as |t|
>
<t.head>
<th class="is-narrow"></th>
<t.sort-by @prop="name">Name</t.sort-by>
@@ -116,13 +163,20 @@
<TaskRow
@data-test-task-row={{row.model.name}}
@task={{row.model}}
@onClick={{action "taskClick" row.model.allocation row.model}} />
@onClick={{action "taskClick" row.model.allocation row.model}}
/>
</t.body>
</ListTable>
{{else}}
<div data-test-empty-tasks-list class="empty-message">
<h3 data-test-empty-tasks-list-headline class="empty-message-headline">No Tasks</h3>
<p data-test-empty-tasks-list-body class="empty-message-body">Allocations will not have tasks until they are in a running state.</p>
<h3
data-test-empty-tasks-list-headline
class="empty-message-headline"
>No Tasks</h3>
<p
data-test-empty-tasks-list-body
class="empty-message-body"
>Allocations will not have tasks until they are in a running state.</p>
</div>
{{/if}}
</div>
@@ -144,7 +198,11 @@
<tr data-test-allocation-port>
<td data-test-allocation-port-name>{{row.model.label}}</td>
<td data-test-allocation-port-address>
<a href="http://{{row.model.hostIp}}:{{row.model.value}}" target="_blank" rel="noopener noreferrer">{{row.model.hostIp}}:{{row.model.value}}</a>
<a
href="http://{{row.model.hostIp}}:{{row.model.value}}"
target="_blank"
rel="noopener noreferrer"
>{{row.model.hostIp}}:{{row.model.value}}</a>
</td>
<td data-test-allocation-port-to>{{row.model.to}}</td>
</tr>
@@ -173,11 +231,21 @@
<tr data-test-service>
<td data-test-service-name>{{row.model.name}}</td>
<td data-test-service-port>{{row.model.portLabel}}</td>
<td data-test-service-tags class="is-long-text">{{join ", " row.model.tags}}</td>
<td data-test-service-tags class="is-long-text">{{join
", "
row.model.tags
}}</td>
<td data-test-service-onupdate>{{row.model.onUpdate}}</td>
<td data-test-service-connect>{{if row.model.connect "Yes" "No"}}</td>
<td data-test-service-connect>{{if
row.model.connect
"Yes"
"No"
}}</td>
<td data-test-service-upstreams>
{{#each row.model.connect.sidecarService.proxy.upstreams as |upstream|}}
{{#each
row.model.connect.sidecarService.proxy.upstreams
as |upstream|
}}
{{upstream.destinationName}}:{{upstream.localBindPort}}
{{/each}}
</td>
@@ -207,48 +275,81 @@
<div class="boxed-section is-small">
<div class="boxed-section-body inline-definitions">
<span class="pair">
<span data-test-allocation-status class="tag {{this.preempter.statusClass}}">
<span
data-test-allocation-status
class="tag {{this.preempter.statusClass}}"
>
{{this.preempter.clientStatus}}
</span>
</span>
<span class="pair">
<span class="term" data-test-allocation-name>{{this.preempter.name}}</span>
<LinkTo @route="allocations.allocation" @model={{this.preempter}} data-test-allocation-id>{{this.preempter.shortId}}</LinkTo>
<span
class="term"
data-test-allocation-name
>{{this.preempter.name}}</span>
<LinkTo
@route="allocations.allocation"
@model={{this.preempter}}
data-test-allocation-id
>{{this.preempter.shortId}}</LinkTo>
</span>
<span class="pair job-link"><span class="term">Job</span>
<LinkTo @route="jobs.job" @model={{this.preempter.job}} @query={{hash jobNamespace=this.preempter.job.namespace.id}} data-test-job-link>{{this.preempter.job.name}}</LinkTo>
<LinkTo
@route="jobs.job"
@model={{this.preempter.job}}
data-test-job-link
>{{this.preempter.job.name}}</LinkTo>
</span>
<span class="pair job-priority"><span class="term">Priority</span>
<span data-test-job-priority>{{this.preempter.job.priority}}</span>
<span
data-test-job-priority
>{{this.preempter.job.priority}}</span>
</span>
<span class="pair node-link"><span class="term">Client</span>
<LinkTo @route="clients.client" @model={{this.preempter.node}} data-test-client-link>{{this.preempter.node.shortId}}</LinkTo>
<LinkTo
@route="clients.client"
@model={{this.preempter.node}}
data-test-client-link
>{{this.preempter.node.shortId}}</LinkTo>
</span>
<span class="pair"><span class="term">Reserved CPU</span>
<span data-test-allocation-cpu>{{format-scheduled-hertz this.preempter.resources.cpu}}</span>
<span data-test-allocation-cpu>{{format-scheduled-hertz
this.preempter.resources.cpu
}}</span>
</span>
<span class="pair"><span class="term">Reserved Memory</span>
<span data-test-allocation-memory>{{format-scheduled-bytes this.preempter.resources.memory start="MiB"}}</span>
<span data-test-allocation-memory>{{format-scheduled-bytes
this.preempter.resources.memory
start="MiB"
}}</span>
</span>
</div>
</div>
{{else}}
<div class="empty-message">
<h3 class="empty-message-headline">Allocation is gone</h3>
<p class="empty-message-body">This allocation has been stopped and garbage collected.</p>
<p class="empty-message-body">This allocation has been stopped and
garbage collected.</p>
</div>
{{/if}}
</div>
</div>
{{/if}}
{{#if (and this.model.preemptedAllocations.isFulfilled this.model.preemptedAllocations.length)}}
{{#if
(and
this.model.preemptedAllocations.isFulfilled
this.model.preemptedAllocations.length
)
}}
<div class="boxed-section" data-test-preemptions>
<div class="boxed-section-head">Preempted Allocations</div>
<div class="boxed-section-body">
<ListTable
@source={{this.model.preemptedAllocations}}
@class="allocations is-isolated" as |t|>
@class="allocations is-isolated"
as |t|
>
<t.head>
<th class="is-narrow"></th>
<th>ID</th>
@@ -262,7 +363,11 @@
<th>Memory</th>
</t.head>
<t.body as |row|>
<AllocationRow @allocation={{row.model}} @context="job" @data-test-allocation={{row.model.id}} />
<AllocationRow
@allocation={{row.model}}
@context="job"
@data-test-allocation={{row.model.id}}
/>
</t.body>
</ListTable>
</div>

View File

@@ -5,23 +5,40 @@
<div data-test-inline-error class="notification is-danger">
<div class="columns">
<div class="column">
<h3 data-test-inline-error-title class="title is-4">{{this.error.title}}</h3>
<p data-test-inline-error-body>{{this.error.description}}</p>
<h3 data-test-inline-error-title class="title is-4">
{{this.error.title}}
</h3>
<p data-test-inline-error-body>
{{this.error.description}}
</p>
</div>
<div class="column is-centered is-minimum">
<button data-test-inline-error-close class="button is-danger" onclick={{action this.onDismiss}} type="button">Okay</button>
<button
data-test-inline-error-close
class="button is-danger"
onclick={{action this.onDismiss}}
type="button"
>
Okay
</button>
</div>
</div>
</div>
{{/if}}
<h1 class="title with-flex" data-test-title>
<div>
{{this.model.name}}
{{#if this.model.isConnectProxy}}
<ProxyTag @class="bumper-left" />
{{/if}}
<span class="{{unless this.model.isConnectProxy "bumper-left"}} tag {{this.model.stateClass}}" data-test-state>{{this.model.state}}</span>
<span
class="{{unless this.model.isConnectProxy "bumper-left"}}
tag
{{this.model.stateClass}}"
data-test-state
>
{{this.model.state}}
</span>
</div>
<div>
{{#if this.model.isRunning}}
@@ -30,7 +47,8 @@
@job={{this.model.task.taskGroup.job}}
@taskGroup={{this.model.task.taskGroup}}
@allocation={{this.model.allocation}}
@task={{this.model.task}} />
@task={{this.model.task}}
/>
</div>
<TwoStepButton
data-test-restart
@@ -41,35 +59,46 @@
@confirmationMessage="Are you sure? This will restart the task in-place."
@awaitingConfirmation={{this.restartTask.isRunning}}
@disabled={{this.restartTask.isRunning}}
@onConfirm={{perform this.restartTask}} />
@onConfirm={{perform this.restartTask}}
/>
{{/if}}
</div>
</h1>
<div class="boxed-section is-small">
<div class="boxed-section-body inline-definitions">
<span class="label">Task Details</span>
<span class="label">
Task Details
</span>
<span class="pair" data-test-started-at>
<span class="term">Started At</span>
<span class="term">
Started At
</span>
{{format-ts this.model.startedAt}}
</span>
{{#if this.model.finishedAt}}
<span class="pair">
<span class="term">Finished At</span>
<span class="term">
Finished At
</span>
{{format-ts this.model.finishedAt}}
</span>
{{/if}}
<span class="pair">
<span class="term">Driver</span>
<span class="term">
Driver
</span>
{{this.model.task.driver}}
</span>
<span class="pair">
<span class="term">Lifecycle</span>
<span data-test-lifecycle>{{this.model.task.lifecycleName}}</span>
<span class="term">
Lifecycle
</span>
<span data-test-lifecycle>
{{this.model.task.lifecycleName}}
</span>
</span>
</div>
</div>
<div class="boxed-section">
<div class="boxed-section-head is-hollow">
Resource Utilization
@@ -86,13 +115,16 @@
</div>
{{else}}
<div data-test-resource-error class="empty-message">
<h3 data-test-resource-error-headline class="empty-message-headline">Task isn't running</h3>
<p class="empty-message-body">Only running tasks utilize resources.</p>
<h3 data-test-resource-error-headline class="empty-message-headline">
Task isn't running
</h3>
<p class="empty-message-body">
Only running tasks utilize resources.
</p>
</div>
{{/if}}
</div>
</div>
{{#if this.model.task.volumeMounts.length}}
<div data-test-volumes class="boxed-section">
<div class="boxed-section-head">
@@ -101,20 +133,40 @@
<div class="boxed-section-body is-full-bleed">
<ListTable @source={{this.model.task.volumeMounts}} as |t|>
<t.head>
<th>Name</th>
<th>Destination</th>
<th>Permissions</th>
<th>Client Source</th>
<th>
Name
</th>
<th>
Destination
</th>
<th>
Permissions
</th>
<th>
Client Source
</th>
</t.head>
<t.body as |row|>
<tr data-test-volume>
<td data-test-volume-name>{{row.model.volume}}</td>
<td data-test-volume-destination><code>{{row.model.destination}}</code></td>
<td data-test-volume-permissions>{{if row.model.readOnly "Read" "Read/Write"}}</td>
<td data-test-volume-name>
{{row.model.volume}}
</td>
<td data-test-volume-destination>
<code>
{{row.model.destination}}
</code>
</td>
<td data-test-volume-permissions>
{{if row.model.readOnly "Read" "Read/Write"}}
</td>
<td data-test-volume-client-source>
{{#if row.model.isCSI}}
<LinkTo @route="csi.volumes.volume" @model={{row.model.source}} @query={{hash volumeNamespace=row.model.namespace.id}}>
{{row.model.source}}
<LinkTo
@route="csi.volumes.volume"
@model={{concat row.model.volume "@" row.model.namespace.id
}}
>
{{row.model.volume}}
</LinkTo>
{{else}}
{{row.model.source}}
@@ -126,27 +178,41 @@
</div>
</div>
{{/if}}
<div class="boxed-section">
<div class="boxed-section-head">
Recent Events
</div>
<div class="boxed-section-body is-full-bleed">
<ListTable @source={{reverse this.model.events}} @class="is-striped" as |t|>
<ListTable
@source={{reverse this.model.events}}
@class="is-striped" as |t|
>
<t.head>
<th class="is-3">Time</th>
<th class="is-1">Type</th>
<th>Description</th>
<th class="is-3">
Time
</th>
<th class="is-1">
Type
</th>
<th>
Description
</th>
</t.head>
<t.body as |row|>
<tr data-test-task-event>
<td data-test-task-event-time>{{format-ts row.model.time}}</td>
<td data-test-task-event-type>{{row.model.type}}</td>
<td data-test-task-event-time>
{{format-ts row.model.time}}
</td>
<td data-test-task-event-type>
{{row.model.type}}
</td>
<td data-test-task-event-message>
{{#if row.model.message}}
{{row.model.message}}
{{else}}
<em>No message</em>
<em>
No message
</em>
{{/if}}
</td>
</tr>
@@ -154,4 +220,4 @@
</ListTable>
</div>
</div>
</section>
</section>

View File

@@ -24,12 +24,20 @@
</a>
{{/if}}
{{#if this.system.agent.config.UI.Consul.BaseUIURL}}
<a data-test-header-consul-link href={{this.system.agent.config.UI.Consul.BaseUIURL}} class="navbar-item">
<a
data-test-header-consul-link
href={{this.system.agent.config.UI.Consul.BaseUIURL}}
class="navbar-item"
>
Consul
</a>
{{/if}}
{{#if this.system.agent.config.UI.Vault.BaseUIURL}}
<a data-test-header-vault-link href={{this.system.agent.config.UI.Vault.BaseUIURL}} class="navbar-item">
<a
data-test-header-vault-link
href={{this.system.agent.config.UI.Vault.BaseUIURL}}
class="navbar-item"
>
Vault
</a>
{{/if}}
@@ -50,4 +58,4 @@
{{yield}}
</ul>
</nav>
</div>
</div>

View File

@@ -1,8 +1,7 @@
<td data-test-job-name>
<LinkTo
@route="jobs.job"
@model={{this.job.plainId}}
@query={{hash namespace=this.job.namespace.id}}
@route="jobs.job.index"
@model={{this.job.idWithNamespace}}
class="is-primary"
>
{{this.job.name}}
@@ -47,7 +46,10 @@
</em>
{{/if}}
{{else}}
<AllocationStatusBar @allocationContainer={{this.job}} @isNarrow={{true}} />
<AllocationStatusBar
@allocationContainer={{this.job}}
@isNarrow={{true}}
/>
{{/if}}
</div>
</td>

View File

@@ -3,7 +3,6 @@
<li data-test-tab="overview">
<LinkTo
@route="jobs.job.index"
@query={{hash namespace=@job.namespace.id}}
@model={{@job}}
@activeClass="is-active"
@current-when="jobs.job.index jobs.job.dispatch"
@@ -14,7 +13,6 @@
<li data-test-tab="definition">
<LinkTo
@route="jobs.job.definition"
@query={{hash namespace=@job.namespace.id}}
@model={{@job}}
@activeClass="is-active"
>
@@ -24,7 +22,6 @@
<li data-test-tab="versions">
<LinkTo
@route="jobs.job.versions"
@query={{hash namespace=@job.namespace.id}}
@model={{@job}}
@activeClass="is-active"
>
@@ -35,7 +32,6 @@
<li data-test-tab="deployments">
<LinkTo
@route="jobs.job.deployments"
@query={{hash namespace=@job.namespace.id}}
@model={{@job}}
@activeClass="is-active"
>
@@ -46,7 +42,6 @@
<li data-test-tab="allocations">
<LinkTo
@route="jobs.job.allocations"
@query={{hash namespace=@job.namespace.id}}
@model={{@job}}
@activeClass="is-active"
>
@@ -56,7 +51,6 @@
<li data-test-tab="evaluations">
<LinkTo
@route="jobs.job.evaluations"
@query={{hash namespace=@job.namespace.id}}
@model={{@job}}
@activeClass="is-active"
>
@@ -67,7 +61,6 @@
<li data-test-tab="clients">
<LinkTo
@route="jobs.job.clients"
@query={{hash namespace=@job.namespace.id}}
@model={{@job}}
@activeClass="is-active"
>

View File

@@ -1,34 +1,54 @@
<td class="is-narrow">
{{#unless this.task.driverStatus.healthy}}
<span data-test-icon="unhealthy-driver" class="tooltip text-center" aria-label="{{this.task.driver}} is unhealthy">
<span
data-test-icon="unhealthy-driver"
class="tooltip text-center"
aria-label="{{this.task.driver}} is unhealthy"
>
{{x-icon "alert-triangle" class="is-warning"}}
</span>
{{/unless}}
</td>
<td data-test-name class="nowrap">
<LinkTo @route="allocations.allocation.task" @models={{array this.task.allocation this.task}} class="is-primary">
<LinkTo
@route="allocations.allocation.task"
@models={{array this.task.allocation this.task}}
class="is-primary"
>
{{this.task.name}}
{{#if this.task.isConnectProxy}}
<ProxyTag @class="bumper-left" />
{{/if}}
</LinkTo>
</td>
<td data-test-state>{{this.task.state}}</td>
<td data-test-state>
{{this.task.state}}
</td>
<td data-test-message>
{{#if this.task.events.lastObject.message}}
{{this.task.events.lastObject.message}}
{{else}}
<em>No message</em>
<em>
No message
</em>
{{/if}}
</td>
<td data-test-time>{{format-ts this.task.events.lastObject.time}}</td>
<td data-test-time>
{{format-ts this.task.events.lastObject.time}}
</td>
<td data-test-volumes>
<ul>
{{#each this.task.task.volumeMounts as |volume|}}
<li data-test-volume>
<strong>{{volume.volume}}:</strong>
<strong>
{{volume.volume}}
:
</strong>
{{#if volume.isCSI}}
<LinkTo @route="csi.volumes.volume" @model={{volume.source}} @query={{hash volumeNamespace=volume.namespace.id}}>
<LinkTo
@route="csi.volumes.volume"
@model={{concat volume.source "@" volume.namespace.id}}
>
{{volume.source}}
</LinkTo>
{{else}}
@@ -43,15 +63,26 @@
{{#if (and (not this.cpu) this.fetchStats.isRunning)}}
...
{{else if this.statsError}}
<span class="tooltip text-center" role="tooltip" aria-label="Couldn't collect stats">
<span
class="tooltip text-center"
role="tooltip"
aria-label="Couldn't collect stats"
>
{{x-icon "alert-triangle" class="is-warning"}}
</span>
{{else}}
<div class="inline-chart is-small tooltip" role="tooltip" aria-label="{{format-hertz this.cpu.used}} / {{format-hertz this.taskStats.reservedCPU}}">
<div
class="inline-chart is-small tooltip"
role="tooltip"
aria-label="{{format-hertz this.cpu.used}}
/
{{format-hertz this.taskStats.reservedCPU}}"
>
<progress
class="progress is-info is-small"
value="{{this.cpu.percent}}"
max="1">
max="1"
>
{{this.cpu.percent}}
</progress>
</div>
@@ -63,18 +94,29 @@
{{#if (and (not this.memory) this.fetchStats.isRunning)}}
...
{{else if this.statsError}}
<span class="tooltip is-small text-center" role="tooltip" aria-label="Couldn't collect stats">
<span
class="tooltip is-small text-center"
role="tooltip"
aria-label="Couldn't collect stats"
>
{{x-icon "alert-triangle" class="is-warning"}}
</span>
{{else}}
<div class="inline-chart tooltip" role="tooltip" aria-label="{{format-bytes this.memory.used}} / {{format-bytes this.taskStats.reservedMemory start='MiB'}}">
<div
class="inline-chart tooltip"
role="tooltip"
aria-label="{{format-bytes this.memory.used}}
/
{{format-bytes this.taskStats.reservedMemory start="MiB"}}"
>
<progress
class="progress is-danger is-small"
value="{{this.memory.percent}}"
max="1">
max="1"
>
{{this.memory.percent}}
</progress>
</div>
{{/if}}
{{/if}}
</td>
</td>

View File

@@ -1,8 +1,16 @@
{{page-title "CSI Volumes"}}
<div class="tabs is-subnav">
<ul>
<li data-test-tab="volumes"><LinkTo @route="csi.volumes.index" @activeClass="is-active">Volumes</LinkTo></li>
<li data-test-tab="plugins"><LinkTo @route="csi.plugins.index" @activeClass="is-active">Plugins</LinkTo></li>
<li data-test-tab="volumes">
<LinkTo @route="csi.volumes.index" @activeClass="is-active">
Volumes
</LinkTo>
</li>
<li data-test-tab="plugins">
<LinkTo @route="csi.plugins.index" @activeClass="is-active">
Plugins
</LinkTo>
</li>
</ul>
</div>
<section class="section">
@@ -13,7 +21,8 @@
data-test-volumes-search
@searchTerm={{mut this.searchTerm}}
@onChange={{action this.resetPagination}}
@placeholder="Search volumes..." />
@placeholder="Search volumes..."
/>
{{/if}}
</div>
{{#if this.system.shouldShowNamespaces}}
@@ -24,101 +33,154 @@
@label="Namespace"
@options={{this.optionsNamespaces}}
@selection={{this.qpNamespace}}
@onSelect={{action (queue
(action this.cacheNamespace)
(action this.setFacetQueryParam "qpNamespace")
)}} />
@onSelect={{action
(queue (action this.cacheNamespace) (action this.setFacetQueryParam "qpNamespace"))
}}
/>
</div>
</div>
{{/if}}
</div>
{{#if this.isForbidden}}
<ForbiddenMessage />
{{else}}
{{#if this.sortedVolumes}}
<ListPagination
@source={{this.sortedVolumes}}
@size={{this.pageSize}}
@page={{this.currentPage}} as |p|>
<ListTable
@source={{p.list}}
@sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}}
@class="with-foot" as |t|>
<t.head>
<t.sort-by @prop="name">Name</t.sort-by>
{{else if this.sortedVolumes}}
<ListPagination
@source={{this.sortedVolumes}}
@size={{this.pageSize}}
@page={{this.currentPage}} as |p|
>
<ListTable
@source={{p.list}}
@sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}}
@class="with-foot" as |t|
>
<t.head>
<t.sort-by @prop="name">
Name
</t.sort-by>
{{#if this.system.shouldShowNamespaces}}
<t.sort-by @prop="namespace.name">
Namespace
</t.sort-by>
{{/if}}
<t.sort-by @prop="schedulable">
Volume Health
</t.sort-by>
<t.sort-by @prop="controllersHealthyProportion">
Controller Health
</t.sort-by>
<t.sort-by @prop="nodesHealthyProportion">
Node Health
</t.sort-by>
<t.sort-by @prop="provider">
Provider
</t.sort-by>
<th>
# Allocs
</th>
</t.head>
<t.body @key="model.name" as |row|>
<tr
class="is-interactive"
data-test-volume-row
{{on "click" (action "gotoVolume" row.model)}}
>
<td data-test-volume-name>
<LinkTo
@route="csi.volumes.volume"
@model={{row.model.idWithNamespace}}
class="is-primary"
>
{{row.model.name}}
</LinkTo>
</td>
{{#if this.system.shouldShowNamespaces}}
<t.sort-by @prop="namespace.name">Namespace</t.sort-by>
<td data-test-volume-namespace>
{{row.model.namespace.name}}
</td>
{{/if}}
<t.sort-by @prop="schedulable">Volume Health</t.sort-by>
<t.sort-by @prop="controllersHealthyProportion">Controller Health</t.sort-by>
<t.sort-by @prop="nodesHealthyProportion">Node Health</t.sort-by>
<t.sort-by @prop="provider">Provider</t.sort-by>
<th># Allocs</th>
</t.head>
<t.body @key="model.name" as |row|>
<tr class="is-interactive" data-test-volume-row {{on "click" (action "gotoVolume" row.model)}}>
<td data-test-volume-name>
<LinkTo
@route="csi.volumes.volume"
@model={{row.model.plainId}}
@query={{hash volumeNamespace=row.model.namespace.name}}
class="is-primary">
{{row.model.name}}
</LinkTo>
</td>
{{#if this.system.shouldShowNamespaces}}
<td data-test-volume-namespace>{{row.model.namespace.name}}</td>
<td data-test-volume-schedulable>
{{if row.model.schedulable "Schedulable" "Unschedulable"}}
</td>
<td data-test-volume-controller-health>
{{#if row.model.controllerRequired}}
{{if (gt row.model.controllersHealthy 0) "Healthy" "Unhealthy"}}
(
{{row.model.controllersHealthy}}
/
{{row.model.controllersExpected}}
)
{{else if (gt row.model.controllersExpected 0)}}
{{if (gt row.model.controllersHealthy 0) "Healthy" "Unhealthy"}}
(
{{row.model.controllersHealthy}}
/
{{row.model.controllersExpected}}
)
{{else}}
<em class="is-faded">
Node Only
</em>
{{/if}}
<td data-test-volume-schedulable>{{if row.model.schedulable "Schedulable" "Unschedulable"}}</td>
<td data-test-volume-controller-health>
{{#if row.model.controllerRequired}}
{{if (gt row.model.controllersHealthy 0) "Healthy" "Unhealthy"}}
({{row.model.controllersHealthy}}/{{row.model.controllersExpected}})
{{else}}
{{#if (gt row.model.controllersExpected 0)}}
{{if (gt row.model.controllersHealthy 0) "Healthy" "Unhealthy"}}
({{row.model.controllersHealthy}}/{{row.model.controllersExpected}})
{{else}}
<em class="is-faded">Node Only</em>
{{/if}}
{{/if}}
</td>
<td data-test-volume-node-health>
{{if (gt row.model.nodesHealthy 0) "Healthy" "Unhealthy"}}
({{row.model.nodesHealthy}}/{{row.model.nodesExpected}})
</td>
<td data-test-volume-provider>{{row.model.provider}}</td>
<td data-test-volume-allocations>{{row.model.allocationCount}}</td>
</tr>
</t.body>
</ListTable>
<div class="table-foot">
<PageSizeSelect @onChange={{action this.resetPagination}} />
<nav class="pagination">
<div class="pagination-numbers">
{{p.startsAt}}&ndash;{{p.endsAt}} of {{this.sortedVolumes.length}}
</div>
<p.prev @class="pagination-previous">{{x-icon "chevron-left"}}</p.prev>
<p.next @class="pagination-next">{{x-icon "chevron-right"}}</p.next>
<ul class="pagination-list"></ul>
</nav>
</div>
</ListPagination>
{{else}}
<div data-test-empty-volumes-list class="empty-message">
{{#if (eq this.visibleVolumes.length 0)}}
<h3 data-test-empty-volumes-list-headline class="empty-message-headline">No Volumes</h3>
<p class="empty-message-body">
This namespace currently has no CSI Volumes.
</p>
{{else if this.searchTerm}}
<h3 data-test-empty-volumes-list-headline class="empty-message-headline">No Matches</h3>
<p class="empty-message-body">
No volumes match the term <strong>{{this.searchTerm}}</strong>
</p>
{{/if}}
</td>
<td data-test-volume-node-health>
{{if (gt row.model.nodesHealthy 0) "Healthy" "Unhealthy"}}
(
{{row.model.nodesHealthy}}
/
{{row.model.nodesExpected}}
)
</td>
<td data-test-volume-provider>
{{row.model.provider}}
</td>
<td data-test-volume-allocations>
{{row.model.allocationCount}}
</td>
</tr>
</t.body>
</ListTable>
<div class="table-foot">
<PageSizeSelect @onChange={{action this.resetPagination}} />
<nav class="pagination">
<div class="pagination-numbers">
{{p.startsAt}}
{{p.endsAt}}
of
{{this.sortedVolumes.length}}
</div>
<p.prev @class="pagination-previous">
{{x-icon "chevron-left"}}
</p.prev>
<p.next @class="pagination-next">
{{x-icon "chevron-right"}}
</p.next>
<ul class="pagination-list"></ul>
</nav>
</div>
{{/if}}
</ListPagination>
{{else}}
<div data-test-empty-volumes-list class="empty-message">
{{#if (eq this.visibleVolumes.length 0)}}
<h3 data-test-empty-volumes-list-headline class="empty-message-headline">
No Volumes
</h3>
<p class="empty-message-body">
This namespace currently has no CSI Volumes.
</p>
{{else if this.searchTerm}}
<h3 data-test-empty-volumes-list-headline class="empty-message-headline">
No Matches
</h3>
<p class="empty-message-body">
No volumes match the term
<strong>
{{this.searchTerm}}
</strong>
</p>
{{/if}}
</div>
{{/if}}
</section>
</section>

View File

@@ -44,7 +44,10 @@
@options={{this.optionsNamespaces}}
@selection={{this.qpNamespace}}
@onSelect={{action
(queue (action this.cacheNamespace) (action this.setFacetQueryParam "qpNamespace"))
(queue
(action this.cacheNamespace)
(action this.setFacetQueryParam "qpNamespace")
)
}}
/>
{{/if}}
@@ -109,13 +112,15 @@
<ListPagination
@source={{this.sortedJobs}}
@size={{this.pageSize}}
@page={{this.currentPage}} as |p|
@page={{this.currentPage}}
as |p|
>
<ListTable
@source={{p.list}}
@sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}}
@class="with-foot" as |t|
@class="with-foot"
as |t|
>
<t.head>
<t.sort-by @prop="name">

View File

@@ -2,64 +2,116 @@
{{page-title "Task group " this.model.name " - Job " this.model.job.name}}
<div class="tabs is-subnav">
<ul>
<li><LinkTo @route="jobs.job.task-group" @models={{array this.model.job this.model}} @activeClass="is-active">Overview</LinkTo></li>
<li>
<LinkTo
@route="jobs.job.task-group"
@models={{array this.model.job this.model}}
@activeClass="is-active"
>
Overview
</LinkTo>
</li>
</ul>
</div>
<section class="section">
<h1 class="title with-flex">
<span>{{this.model.name}}</span>
<span>
{{this.model.name}}
</span>
<div>
<Exec::OpenButton @job={{this.model.job}} @taskGroup={{this.model}} />
{{#if this.model.scaling}}
<StepperInput
data-test-task-group-count-stepper
aria-label={{this.tooltipText}}
@min={{this.model.scaling.min}}
@max={{this.model.scaling.max}}
@value={{this.model.count}}
@class="is-primary is-small"
@disabled={{or this.model.job.runningDeployment (cannot "scale job" namespace=this.model.job.namespace.name)}}
@onChange={{action "scaleTaskGroup"}}>
Count
</StepperInput>
<StepperInput
data-test-task-group-count-stepper
aria-label={{this.tooltipText}}
@min={{this.model.scaling.min}}
@max={{this.model.scaling.max}}
@value={{this.model.count}}
@class="is-primary is-small"
@disabled={{or
this.model.job.runningDeployment
(cannot "scale job" namespace=this.model.job.namespace.name)
}}
@onChange={{action "scaleTaskGroup"}}
>
Count
</StepperInput>
{{/if}}
</div>
</h1>
<div class="boxed-section is-small">
<div class="boxed-section-body inline-definitions">
<span class="label">Task Group Details</span>
<span class="pair" data-test-task-group-tasks><span class="term"># Tasks</span> {{this.model.tasks.length}}</span>
<span class="pair" data-test-task-group-cpu><span class="term">Reserved CPU</span> {{format-scheduled-hertz this.model.reservedCPU}}</span>
<span class="label">
Task Group Details
</span>
<span class="pair" data-test-task-group-tasks>
<span class="term">
# Tasks
</span>
{{this.model.tasks.length}}
</span>
<span class="pair" data-test-task-group-cpu>
<span class="term">
Reserved CPU
</span>
{{format-scheduled-hertz this.model.reservedCPU}}
</span>
<span class="pair" data-test-task-group-mem>
<span class="term">Reserved Memory</span>
<span class="term">
Reserved Memory
</span>
{{format-scheduled-bytes this.model.reservedMemory start="MiB"}}
{{#if (gt this.model.reservedMemoryMax this.model.reservedMemory)}}
({{format-scheduled-bytes this.model.reservedMemoryMax start="MiB"}} Max)
({{format-scheduled-bytes this.model.reservedMemoryMax start="MiB"}}Max)
{{/if}}
</span>
<span class="pair" data-test-task-group-disk><span class="term">Reserved Disk</span> {{format-scheduled-bytes this.model.reservedEphemeralDisk start="MiB"}}</span>
{{#if this.model.scaling}}
<span class="pair" data-test-task-group-min><span class="term">Count Range</span>
{{this.model.scaling.min}} to {{this.model.scaling.max}}
<span class="pair" data-test-task-group-disk>
<span class="term">
Reserved Disk
</span>
<span class="pair" data-test-task-group-max><span class="term">Scaling Policy?</span>
{{format-scheduled-bytes this.model.reservedEphemeralDisk start="MiB"}}
</span>
{{#if this.model.scaling}}
<span class="pair" data-test-task-group-min>
<span class="term">
Count Range
</span>
{{this.model.scaling.min}}
to
{{this.model.scaling.max}}
</span>
<span class="pair" data-test-task-group-max>
<span class="term">
Scaling Policy?
</span>
{{if this.model.scaling.policy "Yes" "No"}}
</span>
{{/if}}
</div>
</div>
<div class="boxed-section">
<div class="boxed-section-head">
<div>Allocation Status <span class="badge is-white">{{this.allocations.length}}</span></div>
<div>
Allocation Status
<span class="badge is-white">
{{this.allocations.length}}
</span>
</div>
</div>
<div class="boxed-section-body">
<AllocationStatusBar @allocationContainer={{this.model.summary}} @class="split-view" as |chart|>
<AllocationStatusBar
@allocationContainer={{this.model.summary}}
@class="split-view" as |chart|
>
<ol class="legend">
{{#each chart.data as |datum index|}}
<li class="{{datum.className}} {{if (eq datum.label chart.activeDatum.label) "is-active"}} {{if (eq datum.value 0) "is-empty"}}">
<li
class="{{datum.className}}
{{if (eq datum.label chart.activeDatum.label) "is-active"}}
{{if (eq datum.value 0) "is-empty"}}"
>
<JobPage::Parts::SummaryLegendItem @datum={{datum}} @index={{index}} />
</li>
{{/each}}
@@ -100,62 +152,102 @@
@source={{this.sortedAllocations}}
@size={{this.pageSize}}
@page={{this.currentPage}}
@class="allocations" as |p|>
@class="allocations" as |p|
>
<ListTable
@source={{p.list}}
@sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}}
@class="with-foot" as |t|>
@class="with-foot" as |t|
>
<t.head>
<th class="is-narrow"></th>
<t.sort-by @prop="shortId">ID</t.sort-by>
<t.sort-by @prop="createIndex" @title="Create Index">Created</t.sort-by>
<t.sort-by @prop="modifyIndex" @title="Modify Index">Modified</t.sort-by>
<t.sort-by @prop="statusIndex">Status</t.sort-by>
<t.sort-by @prop="jobVersion">Version</t.sort-by>
<t.sort-by @prop="node.shortId">Client</t.sort-by>
<th>Volume</th>
<th>CPU</th>
<th>Memory</th>
<t.sort-by @prop="shortId">
ID
</t.sort-by>
<t.sort-by @prop="createIndex" @title="Create Index">
Created
</t.sort-by>
<t.sort-by @prop="modifyIndex" @title="Modify Index">
Modified
</t.sort-by>
<t.sort-by @prop="statusIndex">
Status
</t.sort-by>
<t.sort-by @prop="jobVersion">
Version
</t.sort-by>
<t.sort-by @prop="node.shortId">
Client
</t.sort-by>
<th>
Volume
</th>
<th>
CPU
</th>
<th>
Memory
</th>
</t.head>
<t.body @key="model.id" as |row|>
<AllocationRow @data-test-allocation={{row.model.id}} @allocation={{row.model}} @context="taskGroup" @onClick={{action "gotoAllocation" row.model}} />
<AllocationRow
@data-test-allocation={{row.model.id}}
@allocation={{row.model}}
@context="taskGroup"
@onClick={{action "gotoAllocation" row.model}}
/>
</t.body>
</ListTable>
<div class="table-foot">
<PageSizeSelect @onChange={{action this.resetPagination}} />
<nav class="pagination">
<div class="pagination-numbers">
{{p.startsAt}}&ndash;{{p.endsAt}} of {{this.sortedAllocations.length}}
{{p.startsAt}}
{{p.endsAt}}
of
{{this.sortedAllocations.length}}
</div>
<p.prev @class="pagination-previous">{{x-icon "chevron-left"}}</p.prev>
<p.next @class="pagination-next">{{x-icon "chevron-right"}}</p.next>
<p.prev @class="pagination-previous">
{{x-icon "chevron-left"}}
</p.prev>
<p.next @class="pagination-next">
{{x-icon "chevron-right"}}
</p.next>
<ul class="pagination-list"></ul>
</nav>
</div>
</ListPagination>
{{else if this.allocations.length}}
<div class="boxed-section-body">
<div class="empty-message" data-test-empty-allocations-list>
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>
No Matches
</h3>
<p class="empty-message-body">
No allocations match the term
<strong>
{{this.searchTerm}}
</strong>
</p>
</div>
</div>
{{else}}
{{#if this.allocations.length}}
<div class="boxed-section-body">
<div class="empty-message" data-test-empty-allocations-list>
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>No Matches</h3>
<p class="empty-message-body">No allocations match the term <strong>{{this.searchTerm}}</strong></p>
</div>
<div class="boxed-section-body">
<div class="empty-message" data-test-empty-allocations-list>
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>
No Allocations
</h3>
<p class="empty-message-body">
No allocations have been placed.
</p>
</div>
{{else}}
<div class="boxed-section-body">
<div class="empty-message" data-test-empty-allocations-list>
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>No Allocations</h3>
<p class="empty-message-body">No allocations have been placed.</p>
</div>
</div>
{{/if}}
</div>
{{/if}}
</div>
</div>
<LifecycleChart @tasks={{this.model.tasks}} />
{{#if this.model.scaleState.isVisible}}
{{#if this.shouldShowScaleEventTimeline}}
<div data-test-scaling-timeline class="boxed-section">
@@ -167,7 +259,6 @@
</div>
</div>
{{/if}}
<div data-test-scaling-events class="boxed-section">
<div class="boxed-section-head">
Recent Scaling Events
@@ -177,7 +268,6 @@
</div>
</div>
{{/if}}
{{#if this.model.volumes.length}}
<div data-test-volumes class="boxed-section">
<div class="boxed-section-head">
@@ -186,29 +276,46 @@
<div class="boxed-section-body is-full-bleed">
<ListTable @source={{this.model.volumes}} as |t|>
<t.head>
<th>Name</th>
<th>Type</th>
<th>Source</th>
<th>Permissions</th>
<th>
Name
</th>
<th>
Type
</th>
<th>
Source
</th>
<th>
Permissions
</th>
</t.head>
<t.body as |row|>
<tr data-test-volume>
<td data-test-volume-name>
{{#if row.model.isCSI}}
<LinkTo @route="csi.volumes.volume" @model={{row.model.source}} @query={{hash volumeNamespace=row.model.namespace.id}}>
<LinkTo
@route="csi.volumes.volume"
@model={{concat row.model.source "@" row.model.namespace.id}}
>
{{row.model.name}}
</LinkTo>
{{else}}
{{row.model.name}}
{{/if}}
</td>
<td data-test-volume-type>{{row.model.type}}</td>
<td data-test-volume-source>{{row.model.source}}</td>
<td data-test-volume-permissions>{{if row.model.readOnly "Read" "Read/Write"}}</td>
<td data-test-volume-type>
{{row.model.type}}
</td>
<td data-test-volume-source>
{{row.model.source}}
</td>
<td data-test-volume-permissions>
{{if row.model.readOnly "Read" "Read/Write"}}
</td>
</tr>
</t.body>
</ListTable>
</div>
</div>
{{/if}}
</section>
</section>

View File

@@ -25,8 +25,8 @@ module.exports = function (environment) {
APP: {
blockingQueries: true,
mirageScenario: 'topoMedium',
mirageWithNamespaces: false,
mirageScenario: 'smallCluster',
mirageWithNamespaces: true,
mirageWithTokens: true,
mirageWithRegions: true,
showStorybookLink: process.env.STORYBOOK_LINK === 'true',

View File

@@ -499,6 +499,7 @@ module('Acceptance | allocation detail (preemptions)', function (hooks) {
);
await Allocation.visit({ id: allocation.id });
await Allocation.preempter.visitJob();
assert.equal(
currentURL(),

View File

@@ -251,13 +251,16 @@ module('Acceptance | job detail (with namespaces)', function (hooks) {
test('it passes an accessibility audit', async function (assert) {
const namespace = server.db.namespaces.find(job.namespaceId);
await JobDetail.visit({ id: job.id, namespace: namespace.name });
await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
await a11yAudit(assert);
});
test('when there are namespaces, the job detail page states the namespace for the job', async function (assert) {
const namespace = server.db.namespaces.find(job.namespaceId);
await JobDetail.visit({ id: job.id, namespace: namespace.name });
await JobDetail.visit({
id: `${job.id}@${namespace.name}`,
});
assert.ok(
JobDetail.statFor('namespace').text,
@@ -301,7 +304,8 @@ module('Acceptance | job detail (with namespaces)', function (hooks) {
assert.notOk(JobDetail.execButton.isDisabled);
const secondNamespace = server.db.namespaces[1];
await JobDetail.visit({ id: job2.id, namespace: secondNamespace.name });
await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` });
assert.ok(JobDetail.execButton.isDisabled);
});
@@ -322,9 +326,9 @@ module('Acceptance | job detail (with namespaces)', function (hooks) {
});
await JobDetail.visit({
id: job.id,
namespace: server.db.namespaces[1].name,
id: `${job.id}@${server.db.namespaces[1].name}`,
});
assert.notOk(JobDetail.execButton.isDisabled);
});
@@ -338,14 +342,13 @@ module('Acceptance | job detail (with namespaces)', function (hooks) {
});
await JobDetail.visit({
id: job.id,
namespace: server.db.namespaces[1].name,
id: `${job.id}@${server.db.namespaces[1].name}`,
});
assert.notOk(JobDetail.metaTable, 'Meta table not present');
await JobDetail.visit({
id: jobWithMeta.id,
namespace: server.db.namespaces[1].name,
id: `${jobWithMeta.id}@${server.db.namespaces[1].name}`,
});
assert.ok(JobDetail.metaTable, 'Meta table is present');
});
@@ -361,7 +364,8 @@ module('Acceptance | job detail (with namespaces)', function (hooks) {
},
});
await JobDetail.visit({ id: jobFromPack.id, namespace });
await JobDetail.visit({ id: `${jobFromPack.id}@${namespace}` });
assert.ok(JobDetail.packTag, 'Pack tag is present');
assert.equal(
JobDetail.packStatFor('name').text,
@@ -388,8 +392,7 @@ module('Acceptance | job detail (with namespaces)', function (hooks) {
window.localStorage.nomadTokenSecret = managementToken.secretId;
await JobDetail.visit({
id: job.id,
namespace: server.db.namespaces[1].name,
id: `${job.id}@${server.db.namespaces[1].name}`,
});
const groupsWithRecommendations = job.taskGroups.filter((group) =>
@@ -439,8 +442,7 @@ module('Acceptance | job detail (with namespaces)', function (hooks) {
test('resource recommendations are not fetched when the feature doesnt exist', async function (assert) {
window.localStorage.nomadTokenSecret = managementToken.secretId;
await JobDetail.visit({
id: job.id,
namespace: server.db.namespaces[1].name,
id: `${job.id}@${server.db.namespaces[1].name}`,
});
assert.equal(JobDetail.recommendations.length, 0);
@@ -518,10 +520,10 @@ module('Acceptance | job detail (with namespaces)', function (hooks) {
clientToken.save();
window.localStorage.nomadTokenSecret = clientToken.secretId;
await JobDetail.visit({ id: job.id, namespace: namespace.name });
await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
assert.notOk(JobDetail.incrementButton.isDisabled);
await JobDetail.visit({ id: job2.id, namespace: secondNamespace.name });
await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` });
assert.ok(JobDetail.incrementButton.isDisabled);
});
});

View File

@@ -54,12 +54,12 @@ function moduleForJobDispatch(title, jobFactory) {
});
test('it passes an accessibility audit', async function (assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: `${job.id}@${namespace.name}` });
await a11yAudit(assert);
});
test('the dispatch button is displayed with management token', async function (assert) {
await JobDetail.visit({ id: job.id, namespace: namespace.name });
await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
assert.notOk(JobDetail.dispatchButton.isDisabled);
});
@@ -82,7 +82,7 @@ function moduleForJobDispatch(title, jobFactory) {
clientToken.policyIds = [policy.id];
clientToken.save();
await JobDetail.visit({ id: job.id, namespace: namespace.name });
await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
assert.notOk(JobDetail.dispatchButton.isDisabled);
// Reset clientToken policies.
@@ -93,12 +93,12 @@ function moduleForJobDispatch(title, jobFactory) {
test('the dispatch button is disabled when not allowed', async function (assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;
await JobDetail.visit({ id: job.id, namespace: namespace.name });
await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
assert.ok(JobDetail.dispatchButton.isDisabled);
});
test('all meta fields are displayed', async function (assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: `${job.id}@${namespace.name}` });
assert.equal(
JobDispatch.metaFields.length,
job.parameterizedJob.MetaOptional.length +
@@ -107,7 +107,7 @@ function moduleForJobDispatch(title, jobFactory) {
});
test('required meta fields are properly indicated', async function (assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: `${job.id}@${namespace.name}` });
JobDispatch.metaFields.forEach((f) => {
const hasIndicator = f.label.includes(REQUIRED_INDICATOR);
@@ -136,10 +136,7 @@ function moduleForJobDispatch(title, jobFactory) {
},
});
await JobDispatch.visit({
id: jobWithoutMeta.id,
namespace: namespace.name,
});
await JobDispatch.visit({ id: `${jobWithoutMeta.id}@${namespace.name}` });
assert.ok(JobDispatch.dispatchButton.isPresent);
});
@@ -147,7 +144,7 @@ function moduleForJobDispatch(title, jobFactory) {
job.parameterizedJob.Payload = 'forbidden';
job.save();
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: `${job.id}@${namespace.name}` });
assert.ok(JobDispatch.payload.emptyMessage.isPresent);
assert.notOk(JobDispatch.payload.editor.isPresent);
@@ -170,8 +167,7 @@ function moduleForJobDispatch(title, jobFactory) {
});
await JobDispatch.visit({
id: jobPayloadRequired.id,
namespace: namespace.name,
id: `${jobPayloadRequired.id}@${namespace.name}`,
});
let payloadTitle = JobDispatch.payload.title;
@@ -181,8 +177,7 @@ function moduleForJobDispatch(title, jobFactory) {
);
await JobDispatch.visit({
id: jobPayloadOptional.id,
namespace: namespace.name,
id: `${jobPayloadOptional.id}@${namespace.name}`,
});
payloadTitle = JobDispatch.payload.title;
@@ -199,7 +194,7 @@ function moduleForJobDispatch(title, jobFactory) {
).length;
}
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: `${job.id}@${namespace.name}` });
// Fill form.
JobDispatch.metaFields.map((f) => f.field.input('meta value'));
@@ -222,7 +217,7 @@ function moduleForJobDispatch(title, jobFactory) {
job.parameterizedJob.Payload = 'forbidden';
job.save();
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: `${job.id}@${namespace.name}` });
// Fill only optional meta params.
JobDispatch.optionalMetaFields.map((f) => f.field.input('meta value'));
@@ -237,7 +232,7 @@ function moduleForJobDispatch(title, jobFactory) {
job.parameterizedJob.Payload = 'required';
job.save();
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: `${job.id}@${namespace.name}` });
await JobDispatch.dispatchButton.click();
assert.ok(JobDispatch.hasError, 'Dispatch error message is shown');

View File

@@ -30,7 +30,7 @@ module('Acceptance | job versions', function (hooks) {
const managementToken = server.create('token');
window.localStorage.nomadTokenSecret = managementToken.secretId;
await Versions.visit({ id: job.id, namespace: namespace.id });
await Versions.visit({ id: `${job.id}@${namespace.id}` });
});
test('it passes an accessibility audit', async function (assert) {

View File

@@ -375,7 +375,7 @@ module('Acceptance | task detail (different namespace)', function (hooks) {
await Layout.breadcrumbFor('jobs.job.index').visit();
assert.equal(
currentURL(),
`/jobs/${job.id}?namespace=other-namespace`,
`/jobs/${job.id}@other-namespace`,
'Job breadcrumb links correctly'
);
@@ -383,7 +383,7 @@ module('Acceptance | task detail (different namespace)', function (hooks) {
await Layout.breadcrumbFor('jobs.job.task-group').visit();
assert.equal(
currentURL(),
`/jobs/${job.id}/${taskGroup}?namespace=other-namespace`,
`/jobs/${job.id}@other-namespace/${taskGroup}`,
'Task Group breadcrumb links correctly'
);

View File

@@ -104,7 +104,7 @@ module('Acceptance | task group detail', function (hooks) {
totalMemoryMaxAddendum = ` (${formatScheduledBytes(
totalMemoryMax,
'MiB'
)} Max)`;
)}Max)`;
}
assert.equal(
@@ -232,25 +232,23 @@ module('Acceptance | task group detail', function (hooks) {
window.localStorage.nomadTokenSecret = clientToken.secretId;
await TaskGroup.visit({
id: job.id,
id: `${job.id}@${SCALE_AND_WRITE_NAMESPACE}`,
name: scalingGroup.name,
namespace: SCALE_AND_WRITE_NAMESPACE,
});
assert.equal(
currentURL(),
`/jobs/${job.id}/scaling?namespace=${SCALE_AND_WRITE_NAMESPACE}`
decodeURIComponent(currentURL()),
`/jobs/${job.id}@${SCALE_AND_WRITE_NAMESPACE}/scaling`
);
assert.notOk(TaskGroup.countStepper.increment.isDisabled);
await TaskGroup.visit({
id: job2.id,
id: `${job2.id}@${secondNamespace.name}`,
name: scalingGroup2.name,
namespace: secondNamespace.name,
});
assert.equal(
currentURL(),
`/jobs/${job2.id}/scaling?namespace=${READ_ONLY_NAMESPACE}`
decodeURIComponent(currentURL()),
`/jobs/${job2.id}@${READ_ONLY_NAMESPACE}/scaling`
);
assert.ok(TaskGroup.countStepper.increment.isDisabled);
});

View File

@@ -34,12 +34,12 @@ module('Acceptance | volume detail', function (hooks) {
});
test('it passes an accessibility audit', async function (assert) {
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
await a11yAudit(assert);
});
test('/csi/volumes/:id should have a breadcrumb trail linking back to Volumes and Storage', async function (assert) {
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
assert.equal(Layout.breadcrumbFor('csi.index').text, 'Storage');
assert.equal(Layout.breadcrumbFor('csi.volumes').text, 'Volumes');
@@ -47,14 +47,14 @@ module('Acceptance | volume detail', function (hooks) {
});
test('/csi/volumes/:id should show the volume name in the title', async function (assert) {
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
assert.equal(document.title, `CSI Volume ${volume.name} - Nomad`);
assert.equal(VolumeDetail.title, volume.name);
});
test('/csi/volumes/:id should list additional details for the volume below the title', async function (assert) {
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
assert.ok(
VolumeDetail.health.includes(
@@ -75,7 +75,7 @@ module('Acceptance | volume detail', function (hooks) {
writeAllocations.forEach((alloc) => assignWriteAlloc(volume, alloc));
readAllocations.forEach((alloc) => assignReadAlloc(volume, alloc));
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
assert.equal(VolumeDetail.writeAllocations.length, writeAllocations.length);
writeAllocations
@@ -95,7 +95,7 @@ module('Acceptance | volume detail', function (hooks) {
writeAllocations.forEach((alloc) => assignWriteAlloc(volume, alloc));
readAllocations.forEach((alloc) => assignReadAlloc(volume, alloc));
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
assert.equal(VolumeDetail.readAllocations.length, readAllocations.length);
readAllocations
@@ -126,7 +126,7 @@ module('Acceptance | volume detail', function (hooks) {
0
);
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
VolumeDetail.writeAllocations.objectAt(0).as((allocationRow) => {
assert.equal(
@@ -198,28 +198,28 @@ module('Acceptance | volume detail', function (hooks) {
const allocation = server.create('allocation');
assignWriteAlloc(volume, allocation);
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
await VolumeDetail.writeAllocations.objectAt(0).visit();
assert.equal(currentURL(), `/allocations/${allocation.id}`);
});
test('when there are no write allocations, the table presents an empty state', async function (assert) {
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
assert.ok(VolumeDetail.writeTableIsEmpty);
assert.equal(VolumeDetail.writeEmptyState.headline, 'No Write Allocations');
});
test('when there are no read allocations, the table presents an empty state', async function (assert) {
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
assert.ok(VolumeDetail.readTableIsEmpty);
assert.equal(VolumeDetail.readEmptyState.headline, 'No Read Allocations');
});
test('the constraints table shows access mode and attachment mode', async function (assert) {
await VolumeDetail.visit({ id: volume.id });
await VolumeDetail.visit({ id: `${volume.id}@default` });
assert.equal(VolumeDetail.constraints.accessMode, volume.accessMode);
assert.equal(
@@ -244,7 +244,7 @@ module('Acceptance | volume detail (with namespaces)', function (hooks) {
});
test('/csi/volumes/:id detail ribbon includes the namespace of the volume', async function (assert) {
await VolumeDetail.visit({ id: volume.id, namespace: volume.namespaceId });
await VolumeDetail.visit({ id: `${volume.id}@${volume.namespaceId}` });
assert.ok(VolumeDetail.hasNamespace);
assert.ok(VolumeDetail.namespace.includes(volume.namespaceId || 'default'));

View File

@@ -79,7 +79,7 @@ module('Acceptance | volumes list', function (hooks) {
const isHealthy = healthy > 0;
controllerHealthStr = `${
isHealthy ? 'Healthy' : 'Unhealthy'
} (${healthy}/${expected})`;
} ( ${healthy} / ${expected} )`;
}
const nodeHealthStr = volume.nodesHealthy > 0 ? 'Healthy' : 'Unhealthy';
@@ -93,7 +93,7 @@ module('Acceptance | volumes list', function (hooks) {
assert.equal(volumeRow.controllerHealth, controllerHealthStr);
assert.equal(
volumeRow.nodeHealth,
`${nodeHealthStr} (${volume.nodesHealthy}/${volume.nodesExpected})`
`${nodeHealthStr} ( ${volume.nodesHealthy} / ${volume.nodesExpected} )`
);
assert.equal(volumeRow.provider, volume.provider);
assert.equal(volumeRow.allocations, readAllocs.length + writeAllocs.length);
@@ -110,7 +110,7 @@ module('Acceptance | volumes list', function (hooks) {
await VolumesList.volumes.objectAt(0).clickName();
assert.equal(
currentURL(),
`/csi/volumes/${volume.id}?namespace=${secondNamespace.id}`
`/csi/volumes/${volume.id}@${secondNamespace.id}`
);
await VolumesList.visit({ namespace: '*' });
@@ -119,7 +119,7 @@ module('Acceptance | volumes list', function (hooks) {
await VolumesList.volumes.objectAt(0).clickRow();
assert.equal(
currentURL(),
`/csi/volumes/${volume.id}?namespace=${secondNamespace.id}`
`/csi/volumes/${volume.id}@${secondNamespace.id}`
);
});

View File

@@ -41,7 +41,7 @@ export default function moduleForJob(
if (!job.namespace || job.namespace === 'default') {
await JobDetail.visit({ id: job.id });
} else {
await JobDetail.visit({ id: job.id, namespace: job.namespace });
await JobDetail.visit({ id: `${job.id}@${job.namespace}` });
}
const hasClientStatus = ['system', 'sysbatch'].includes(job.type);
@@ -51,52 +51,52 @@ export default function moduleForJob(
});
test('visiting /jobs/:job_id', async function (assert) {
assert.equal(
currentURL(),
urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}`, job.namespace)
);
const expectedURL = job.namespace
? `/jobs/${job.name}@${job.namespace}`
: `/jobs/${job.name}`;
assert.equal(decodeURIComponent(currentURL()), expectedURL);
assert.equal(document.title, `Job ${job.name} - Nomad`);
});
test('the subnav links to overview', async function (assert) {
await JobDetail.tabFor('overview').visit();
assert.equal(
currentURL(),
urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}`, job.namespace)
);
const expectedURL = job.namespace
? `/jobs/${job.name}@${job.namespace}`
: `/jobs/${job.name}`;
assert.equal(decodeURIComponent(currentURL()), expectedURL);
});
test('the subnav links to definition', async function (assert) {
await JobDetail.tabFor('definition').visit();
assert.equal(
currentURL(),
urlWithNamespace(
`/jobs/${encodeURIComponent(job.id)}/definition`,
job.namespace
)
);
const expectedURL = job.namespace
? `/jobs/${job.name}@${job.namespace}/definition`
: `/jobs/${job.name}/definition`;
assert.equal(decodeURIComponent(currentURL()), expectedURL);
});
test('the subnav links to versions', async function (assert) {
await JobDetail.tabFor('versions').visit();
assert.equal(
currentURL(),
urlWithNamespace(
`/jobs/${encodeURIComponent(job.id)}/versions`,
job.namespace
)
);
const expectedURL = job.namespace
? `/jobs/${job.name}@${job.namespace}/versions`
: `/jobs/${job.name}/versions`;
assert.equal(decodeURIComponent(currentURL()), expectedURL);
});
test('the subnav links to evaluations', async function (assert) {
await JobDetail.tabFor('evaluations').visit();
assert.equal(
currentURL(),
urlWithNamespace(
`/jobs/${encodeURIComponent(job.id)}/evaluations`,
job.namespace
)
);
const expectedURL = job.namespace
? `/jobs/${job.name}@${job.namespace}/evaluations`
: `/jobs/${job.name}/evaluations`;
assert.equal(decodeURIComponent(currentURL()), expectedURL);
});
test('the title buttons are dependent on job status', async function (assert) {
@@ -145,7 +145,7 @@ export default function moduleForJob(
const encodedStatus = encodeURIComponent(JSON.stringify([status]));
const expectedURL = new URL(
urlWithNamespace(
`/jobs/${job.name}/clients?status=${encodedStatus}`,
`/jobs/${job.name}@default/clients?status=${encodedStatus}`,
job.namespace
),
window.location
@@ -248,13 +248,12 @@ export function moduleForJobWithClientStatus(
test('the subnav links to clients', async function (assert) {
await JobDetail.tabFor('clients').visit();
assert.equal(
currentURL(),
urlWithNamespace(
`/jobs/${encodeURIComponent(job.id)}/clients`,
job.namespace
)
);
const expectedURL = job.namespace
? `/jobs/${job.id}@${job.namespace}/clients`
: `/jobs/${job.id}/clients`;
assert.equal(currentURL(), expectedURL);
});
test('job status summary is shown in the overview', async function (assert) {
@@ -289,23 +288,12 @@ export function moduleForJobWithClientStatus(
await slice.click();
const encodedStatus = encodeURIComponent(JSON.stringify([status]));
const expectedURL = new URL(
urlWithNamespace(
`/jobs/${job.name}/clients?status=${encodedStatus}`,
job.namespace
),
window.location
);
const gotURL = new URL(currentURL(), window.location);
assert.deepEqual(gotURL.pathname, expectedURL.pathname);
// Sort and compare URL query params.
gotURL.searchParams.sort();
expectedURL.searchParams.sort();
assert.equal(
gotURL.searchParams.toString(),
expectedURL.searchParams.toString()
);
const expectedURL = job.namespace
? `/jobs/${job.name}@${job.namespace}/clients?status=${encodedStatus}`
: `/jobs/${job.name}/clients?status=${encodedStatus}`;
assert.deepEqual(currentURL(), expectedURL, 'url is correct');
});
for (var testName in additionalTests) {
@@ -368,6 +356,6 @@ async function visitJobDetailPage({ id, namespace }) {
if (!namespace || namespace === 'default') {
await JobDetail.visit({ id });
} else {
await JobDetail.visit({ id, namespace });
await JobDetail.visit({ id: `${id}@${namespace}` });
}
}