github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/tests/acceptance/client-detail-test.js (about)

     1  import { currentURL, waitUntil, settled } from '@ember/test-helpers';
     2  import { assign } from '@ember/polyfills';
     3  import { module, test } from 'qunit';
     4  import { setupApplicationTest } from 'ember-qunit';
     5  import { setupMirage } from 'ember-cli-mirage/test-support';
     6  import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
     7  import { formatBytes } from 'nomad-ui/helpers/format-bytes';
     8  import moment from 'moment';
     9  import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
    10  import Clients from 'nomad-ui/tests/pages/clients/list';
    11  import Jobs from 'nomad-ui/tests/pages/jobs/list';
    12  import Layout from 'nomad-ui/tests/pages/layout';
    13  
    14  let node;
    15  let managementToken;
    16  let clientToken;
    17  
    18  const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation;
    19  
    20  module('Acceptance | client detail', function(hooks) {
    21    setupApplicationTest(hooks);
    22    setupMirage(hooks);
    23  
    24    hooks.beforeEach(function() {
    25      server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
    26      node = server.db.nodes[0];
    27  
    28      managementToken = server.create('token');
    29      clientToken = server.create('token');
    30  
    31      window.localStorage.nomadTokenSecret = managementToken.secretId;
    32  
    33      // Related models
    34      server.create('agent');
    35      server.create('job', { createAllocations: false });
    36      server.createList('allocation', 3);
    37      server.create('allocation', 'preempted');
    38  
    39      // Force all allocations into the running state so now allocation rows are missing
    40      // CPU/Mem runtime metrics
    41      server.schema.allocations.all().models.forEach(allocation => {
    42        allocation.update({ clientStatus: 'running' });
    43      });
    44    });
    45  
    46    test('it passes an accessibility audit', async function(assert) {
    47      await ClientDetail.visit({ id: node.id });
    48      await a11yAudit(assert);
    49    });
    50  
    51    test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) {
    52      await ClientDetail.visit({ id: node.id });
    53  
    54      assert.equal(document.title, `Client ${node.name} - Nomad`);
    55  
    56      assert.equal(
    57        Layout.breadcrumbFor('clients.index').text,
    58        'Clients',
    59        'First breadcrumb says clients'
    60      );
    61      assert.equal(
    62        Layout.breadcrumbFor('clients.client').text,
    63        node.id.split('-')[0],
    64        'Second breadcrumb says the node short id'
    65      );
    66      await Layout.breadcrumbFor('clients.index').visit();
    67      assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients');
    68    });
    69  
    70    test('/clients/:id should list immediate details for the node in the title', async function(assert) {
    71      node = server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible', drain: false });
    72  
    73      await ClientDetail.visit({ id: node.id });
    74  
    75      assert.ok(ClientDetail.title.includes(node.name), 'Title includes name');
    76      assert.ok(ClientDetail.clientId.includes(node.id), 'Title includes id');
    77      assert.equal(
    78        ClientDetail.statusLight.objectAt(0).id,
    79        node.status,
    80        'Title includes status light'
    81      );
    82    });
    83  
    84    test('/clients/:id should list additional detail for the node below the title', async function(assert) {
    85      await ClientDetail.visit({ id: node.id });
    86  
    87      assert.ok(
    88        ClientDetail.statusDefinition.includes(node.status),
    89        'Status is in additional details'
    90      );
    91      assert.ok(
    92        ClientDetail.statusDecorationClass.includes(`node-${node.status}`),
    93        'Status is decorated with a status class'
    94      );
    95      assert.ok(
    96        ClientDetail.addressDefinition.includes(node.httpAddr),
    97        'Address is in additional details'
    98      );
    99      assert.ok(
   100        ClientDetail.datacenterDefinition.includes(node.datacenter),
   101        'Datacenter is in additional details'
   102      );
   103    });
   104  
   105    test('/clients/:id should include resource utilization graphs', async function(assert) {
   106      await ClientDetail.visit({ id: node.id });
   107  
   108      assert.equal(ClientDetail.resourceCharts.length, 2, 'Two resource utilization graphs');
   109      assert.equal(ClientDetail.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU');
   110      assert.equal(ClientDetail.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory');
   111    });
   112  
   113    test('/clients/:id should list all allocations on the node', async function(assert) {
   114      const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length;
   115  
   116      await ClientDetail.visit({ id: node.id });
   117  
   118      assert.equal(
   119        ClientDetail.allocations.length,
   120        allocationsCount,
   121        `Allocations table lists all ${allocationsCount} associated allocations`
   122      );
   123    });
   124  
   125    test('each allocation should have high-level details for the allocation', async function(assert) {
   126      const allocation = server.db.allocations
   127        .where({ nodeId: node.id })
   128        .sortBy('modifyIndex')
   129        .reverse()[0];
   130  
   131      const allocStats = server.db.clientAllocationStats.find(allocation.id);
   132      const taskGroup = server.db.taskGroups.findBy({
   133        name: allocation.taskGroup,
   134        jobId: allocation.jobId,
   135      });
   136  
   137      const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
   138      const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0);
   139      const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0);
   140  
   141      await ClientDetail.visit({ id: node.id });
   142  
   143      const allocationRow = ClientDetail.allocations.objectAt(0);
   144  
   145      assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID');
   146      assert.equal(
   147        allocationRow.createTime,
   148        moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'),
   149        'Allocation create time'
   150      );
   151      assert.equal(
   152        allocationRow.modifyTime,
   153        moment(allocation.modifyTime / 1000000).fromNow(),
   154        'Allocation modify time'
   155      );
   156      assert.equal(allocationRow.status, allocation.clientStatus, 'Client status');
   157      assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
   158      assert.ok(allocationRow.taskGroup, 'Task group name');
   159      assert.ok(allocationRow.jobVersion, 'Job Version');
   160      assert.equal(allocationRow.volume, 'Yes', 'Volume');
   161      assert.equal(
   162        allocationRow.cpu,
   163        Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed,
   164        'CPU %'
   165      );
   166      assert.equal(
   167        allocationRow.cpuTooltip,
   168        `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`,
   169        'Detailed CPU information is in a tooltip'
   170      );
   171      assert.equal(
   172        allocationRow.mem,
   173        allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed,
   174        'Memory used'
   175      );
   176      assert.equal(
   177        allocationRow.memTooltip,
   178        `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`,
   179        'Detailed memory information is in a tooltip'
   180      );
   181    });
   182  
   183    test('each allocation should show job information even if the job is incomplete and already in the store', async function(assert) {
   184      // First, visit clients to load the allocations for each visible node.
   185      // Don't load the job belongsTo of the allocation! Leave it unfulfilled.
   186  
   187      await Clients.visit();
   188  
   189      // Then, visit jobs to load all jobs, which should implicitly fulfill
   190      // the job belongsTo of each allocation pointed at each job.
   191  
   192      await Jobs.visit();
   193  
   194      // Finally, visit a node to assert that the job name and task group name are
   195      // present. This will require reloading the job, since task groups aren't a
   196      // part of the jobs list response.
   197  
   198      await ClientDetail.visit({ id: node.id });
   199  
   200      const allocationRow = ClientDetail.allocations.objectAt(0);
   201      const allocation = server.db.allocations
   202        .where({ nodeId: node.id })
   203        .sortBy('modifyIndex')
   204        .reverse()[0];
   205  
   206      assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
   207      assert.ok(allocationRow.taskGroup.includes(allocation.taskGroup), 'Task group name');
   208    });
   209  
   210    test('each allocation should link to the allocation detail page', async function(assert) {
   211      const allocation = server.db.allocations
   212        .where({ nodeId: node.id })
   213        .sortBy('modifyIndex')
   214        .reverse()[0];
   215  
   216      await ClientDetail.visit({ id: node.id });
   217      await ClientDetail.allocations.objectAt(0).visit();
   218  
   219      assert.equal(
   220        currentURL(),
   221        `/allocations/${allocation.id}`,
   222        'Allocation rows link to allocation detail pages'
   223      );
   224    });
   225  
   226    test('each allocation should link to the job the allocation belongs to', async function(assert) {
   227      await ClientDetail.visit({ id: node.id });
   228  
   229      const allocation = server.db.allocations.where({ nodeId: node.id })[0];
   230      const job = server.db.jobs.find(allocation.jobId);
   231  
   232      await ClientDetail.allocations.objectAt(0).visitJob();
   233  
   234      assert.equal(
   235        currentURL(),
   236        `/jobs/${job.id}`,
   237        'Allocation rows link to the job detail page for the allocation'
   238      );
   239    });
   240  
   241    test('the allocation section should show the count of preempted allocations on the client', async function(assert) {
   242      const allocations = server.db.allocations.where({ nodeId: node.id });
   243  
   244      await ClientDetail.visit({ id: node.id });
   245  
   246      assert.equal(
   247        ClientDetail.allocationFilter.allCount,
   248        allocations.length,
   249        'All filter/badge shows all allocations count'
   250      );
   251      assert.ok(
   252        ClientDetail.allocationFilter.preemptionsCount.startsWith(
   253          allocations.filter(wasPreemptedFilter).length
   254        ),
   255        'Preemptions filter/badge shows preempted allocations count'
   256      );
   257    });
   258  
   259    test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) {
   260      const allocations = server.db.allocations.where({ nodeId: node.id });
   261  
   262      await ClientDetail.visit({ id: node.id });
   263      await ClientDetail.allocationFilter.preemptions();
   264  
   265      assert.equal(
   266        ClientDetail.allocations.length,
   267        allocations.filter(wasPreemptedFilter).length,
   268        'Only preempted allocations are shown'
   269      );
   270      assert.equal(
   271        currentURL(),
   272        `/clients/${node.id}?preemptions=true`,
   273        'Filter is persisted in the URL'
   274      );
   275    });
   276  
   277    test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) {
   278      const allocations = server.db.allocations.where({ nodeId: node.id });
   279  
   280      await ClientDetail.visit({ id: node.id });
   281      await ClientDetail.allocationFilter.preemptions();
   282      await ClientDetail.allocationFilter.all();
   283  
   284      assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown');
   285      assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL');
   286    });
   287  
   288    test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) {
   289      const allocations = server.db.allocations.where({ nodeId: node.id });
   290  
   291      await ClientDetail.visit({ id: node.id, preemptions: true });
   292  
   293      assert.equal(
   294        ClientDetail.allocations.length,
   295        allocations.filter(wasPreemptedFilter).length,
   296        'Only preempted allocations are shown'
   297      );
   298    });
   299  
   300    test('/clients/:id should list all attributes for the node', async function(assert) {
   301      await ClientDetail.visit({ id: node.id });
   302  
   303      assert.ok(ClientDetail.attributesTable, 'Attributes table is on the page');
   304    });
   305  
   306    test('/clients/:id lists all meta attributes', async function(assert) {
   307      node = server.create('node', 'forceIPv4', 'withMeta');
   308  
   309      await ClientDetail.visit({ id: node.id });
   310  
   311      assert.ok(ClientDetail.metaTable, 'Meta attributes table is on the page');
   312      assert.notOk(ClientDetail.emptyMetaMessage, 'Meta attributes is not empty');
   313  
   314      const firstMetaKey = Object.keys(node.meta)[0];
   315      const firstMetaAttribute = ClientDetail.metaAttributes.objectAt(0);
   316      assert.equal(
   317        firstMetaAttribute.key,
   318        firstMetaKey,
   319        'Meta attributes for the node are bound to the attributes table'
   320      );
   321      assert.equal(
   322        firstMetaAttribute.value,
   323        node.meta[firstMetaKey],
   324        'Meta attributes for the node are bound to the attributes table'
   325      );
   326    });
   327  
   328    test('/clients/:id shows an empty message when there is no meta data', async function(assert) {
   329      await ClientDetail.visit({ id: node.id });
   330  
   331      assert.notOk(ClientDetail.metaTable, 'Meta attributes table is not on the page');
   332      assert.ok(ClientDetail.emptyMetaMessage, 'Meta attributes is empty');
   333    });
   334  
   335    test('when the node is not found, an error message is shown, but the URL persists', async function(assert) {
   336      await ClientDetail.visit({ id: 'not-a-real-node' });
   337  
   338      assert.equal(
   339        server.pretender.handledRequests
   340          .filter(request => !request.url.includes('policy'))
   341          .findBy('status', 404).url,
   342        '/v1/node/not-a-real-node',
   343        'A request to the nonexistent node is made'
   344      );
   345      assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists');
   346      assert.ok(ClientDetail.error.isShown, 'Error message is shown');
   347      assert.equal(ClientDetail.error.title, 'Not Found', 'Error message is for 404');
   348    });
   349  
   350    test('/clients/:id shows the recent events list', async function(assert) {
   351      await ClientDetail.visit({ id: node.id });
   352  
   353      assert.ok(ClientDetail.hasEvents, 'Client events section exists');
   354    });
   355  
   356    test('each node event shows basic node event information', async function(assert) {
   357      const event = server.db.nodeEvents
   358        .where({ nodeId: node.id })
   359        .sortBy('time')
   360        .reverse()[0];
   361  
   362      await ClientDetail.visit({ id: node.id });
   363  
   364      const eventRow = ClientDetail.events.objectAt(0);
   365      assert.equal(
   366        eventRow.time,
   367        moment(event.time).format("MMM DD, 'YY HH:mm:ss ZZ"),
   368        'Event timestamp'
   369      );
   370      assert.equal(eventRow.subsystem, event.subsystem, 'Event subsystem');
   371      assert.equal(eventRow.message, event.message, 'Event message');
   372    });
   373  
   374    test('/clients/:id shows the driver status of every driver for the node', async function(assert) {
   375      // Set the drivers up so health and detection is well tested
   376      const nodeDrivers = node.drivers;
   377      const undetectedDriver = 'raw_exec';
   378  
   379      Object.values(nodeDrivers).forEach(driver => {
   380        driver.Detected = true;
   381      });
   382  
   383      nodeDrivers[undetectedDriver].Detected = false;
   384      node.drivers = nodeDrivers;
   385  
   386      const drivers = Object.keys(node.drivers)
   387        .map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
   388        .sortBy('Name');
   389  
   390      assert.ok(drivers.length > 0, 'Node has drivers');
   391  
   392      await ClientDetail.visit({ id: node.id });
   393  
   394      drivers.forEach((driver, index) => {
   395        const driverHead = ClientDetail.driverHeads.objectAt(index);
   396  
   397        assert.equal(driverHead.name, driver.Name, `${driver.Name}: Name is correct`);
   398        assert.equal(
   399          driverHead.detected,
   400          driver.Detected ? 'Yes' : 'No',
   401          `${driver.Name}: Detection is correct`
   402        );
   403        assert.equal(
   404          driverHead.lastUpdated,
   405          moment(driver.UpdateTime).fromNow(),
   406          `${driver.Name}: Last updated shows time since now`
   407        );
   408  
   409        if (driver.Name === undetectedDriver) {
   410          assert.notOk(
   411            driverHead.healthIsShown,
   412            `${driver.Name}: No health for the undetected driver`
   413          );
   414        } else {
   415          assert.equal(
   416            driverHead.health,
   417            driver.Healthy ? 'Healthy' : 'Unhealthy',
   418            `${driver.Name}: Health is correct`
   419          );
   420          assert.ok(
   421            driverHead.healthClass.includes(driver.Healthy ? 'running' : 'failed'),
   422            `${driver.Name}: Swatch with correct class is shown`
   423          );
   424        }
   425      });
   426    });
   427  
   428    test('each driver can be opened to see a message and attributes', async function(assert) {
   429      // Only detected drivers can be expanded
   430      const nodeDrivers = node.drivers;
   431      Object.values(nodeDrivers).forEach(driver => {
   432        driver.Detected = true;
   433      });
   434      node.drivers = nodeDrivers;
   435  
   436      const driver = Object.keys(node.drivers)
   437        .map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
   438        .sortBy('Name')[0];
   439  
   440      await ClientDetail.visit({ id: node.id });
   441      const driverHead = ClientDetail.driverHeads.objectAt(0);
   442      const driverBody = ClientDetail.driverBodies.objectAt(0);
   443  
   444      assert.notOk(driverBody.descriptionIsShown, 'Driver health description is not shown');
   445      assert.notOk(driverBody.attributesAreShown, 'Driver attributes section is not shown');
   446  
   447      await driverHead.toggle();
   448      assert.equal(
   449        driverBody.description,
   450        driver.HealthDescription,
   451        'Driver health description is now shown'
   452      );
   453      assert.ok(driverBody.attributesAreShown, 'Driver attributes section is now shown');
   454    });
   455  
   456    test('the status light indicates when the node is ineligible for scheduling', async function(assert) {
   457      node = server.create('node', {
   458        drain: false,
   459        schedulingEligibility: 'ineligible',
   460        status: 'ready',
   461      });
   462  
   463      await ClientDetail.visit({ id: node.id });
   464  
   465      assert.equal(
   466        ClientDetail.statusLight.objectAt(0).id,
   467        'ineligible',
   468        'Title status light is in the ineligible state'
   469      );
   470    });
   471  
   472    test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', async function(assert) {
   473      const deadline = 5400000000000; // 1.5 hours in nanoseconds
   474      const forceDeadline = moment().add(1, 'd');
   475  
   476      node = server.create('node', {
   477        drain: true,
   478        schedulingEligibility: 'ineligible',
   479        drainStrategy: {
   480          Deadline: deadline,
   481          ForceDeadline: forceDeadline.toISOString(),
   482          IgnoreSystemJobs: false,
   483        },
   484      });
   485  
   486      await ClientDetail.visit({ id: node.id });
   487  
   488      assert.ok(
   489        ClientDetail.drainDetails.deadline.includes(forceDeadline.fromNow(true)),
   490        'Deadline is shown in a human formatted way'
   491      );
   492  
   493      assert.equal(
   494        ClientDetail.drainDetails.deadlineTooltip,
   495        forceDeadline.format("MMM DD, 'YY HH:mm:ss ZZ"),
   496        'The tooltip for deadline shows the force deadline as an absolute date'
   497      );
   498  
   499      assert.ok(
   500        ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'),
   501        'Drain System Jobs state is shown'
   502      );
   503    });
   504  
   505    test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', async function(assert) {
   506      const deadline = 0;
   507  
   508      node = server.create('node', {
   509        drain: true,
   510        schedulingEligibility: 'ineligible',
   511        drainStrategy: {
   512          Deadline: deadline,
   513          ForceDeadline: '0001-01-01T00:00:00Z', // null as a date
   514          IgnoreSystemJobs: true,
   515        },
   516      });
   517  
   518      await ClientDetail.visit({ id: node.id });
   519  
   520      assert.notOk(ClientDetail.drainDetails.durationIsShown, 'Duration is omitted');
   521  
   522      assert.ok(
   523        ClientDetail.drainDetails.deadline.includes('No deadline'),
   524        'The value for Deadline is "no deadline"'
   525      );
   526  
   527      assert.ok(
   528        ClientDetail.drainDetails.drainSystemJobsText.endsWith('No'),
   529        'Drain System Jobs state is shown'
   530      );
   531    });
   532  
   533    test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', async function(assert) {
   534      const deadline = -1;
   535  
   536      node = server.create('node', {
   537        drain: true,
   538        schedulingEligibility: 'ineligible',
   539        drainStrategy: {
   540          Deadline: deadline,
   541          ForceDeadline: '0001-01-01T00:00:00Z', // null as a date
   542          IgnoreSystemJobs: false,
   543        },
   544      });
   545  
   546      await ClientDetail.visit({ id: node.id });
   547  
   548      assert.ok(
   549        ClientDetail.drainDetails.forceDrainText.endsWith('Yes'),
   550        'Forced Drain is described'
   551      );
   552  
   553      assert.ok(ClientDetail.drainDetails.duration.includes('--'), 'Duration is shown but unset');
   554  
   555      assert.ok(ClientDetail.drainDetails.deadline.includes('--'), 'Deadline is shown but unset');
   556  
   557      assert.ok(
   558        ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'),
   559        'Drain System Jobs state is shown'
   560      );
   561    });
   562  
   563    test('toggling node eligibility disables the toggle and sends the correct POST request', async function(assert) {
   564      node = server.create('node', {
   565        drain: false,
   566        schedulingEligibility: 'eligible',
   567      });
   568  
   569      server.pretender.post('/v1/node/:id/eligibility', () => [200, {}, ''], true);
   570  
   571      await ClientDetail.visit({ id: node.id });
   572      assert.ok(ClientDetail.eligibilityToggle.isActive);
   573  
   574      ClientDetail.eligibilityToggle.toggle();
   575      await waitUntil(() => server.pretender.handledRequests.findBy('method', 'POST'));
   576  
   577      assert.ok(ClientDetail.eligibilityToggle.isDisabled);
   578      server.pretender.resolve(server.pretender.requestReferences[0].request);
   579  
   580      await settled();
   581  
   582      assert.notOk(ClientDetail.eligibilityToggle.isActive);
   583      assert.notOk(ClientDetail.eligibilityToggle.isDisabled);
   584  
   585      const request = server.pretender.handledRequests.findBy('method', 'POST');
   586      assert.equal(request.url, `/v1/node/${node.id}/eligibility`);
   587      assert.deepEqual(JSON.parse(request.requestBody), {
   588        NodeID: node.id,
   589        Eligibility: 'ineligible',
   590      });
   591  
   592      ClientDetail.eligibilityToggle.toggle();
   593      await waitUntil(() => server.pretender.handledRequests.filterBy('method', 'POST').length === 2);
   594      server.pretender.resolve(server.pretender.requestReferences[0].request);
   595  
   596      assert.ok(ClientDetail.eligibilityToggle.isActive);
   597      const request2 = server.pretender.handledRequests.filterBy('method', 'POST')[1];
   598  
   599      assert.equal(request2.url, `/v1/node/${node.id}/eligibility`);
   600      assert.deepEqual(JSON.parse(request2.requestBody), {
   601        NodeID: node.id,
   602        Eligibility: 'eligible',
   603      });
   604    });
   605  
   606    test('starting a drain sends the correct POST request', async function(assert) {
   607      let request;
   608  
   609      node = server.create('node', {
   610        drain: false,
   611        schedulingEligibility: 'eligible',
   612      });
   613  
   614      await ClientDetail.visit({ id: node.id });
   615      await ClientDetail.drainPopover.toggle();
   616      await ClientDetail.drainPopover.submit();
   617  
   618      request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
   619  
   620      assert.equal(request.url, `/v1/node/${node.id}/drain`);
   621      assert.deepEqual(
   622        JSON.parse(request.requestBody),
   623        {
   624          NodeID: node.id,
   625          DrainSpec: {
   626            Deadline: 0,
   627            IgnoreSystemJobs: false,
   628          },
   629        },
   630        'Drain with default settings'
   631      );
   632  
   633      await ClientDetail.drainPopover.toggle();
   634      await ClientDetail.drainPopover.deadlineToggle.toggle();
   635      await ClientDetail.drainPopover.submit();
   636  
   637      request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
   638  
   639      assert.deepEqual(
   640        JSON.parse(request.requestBody),
   641        {
   642          NodeID: node.id,
   643          DrainSpec: {
   644            Deadline: 60 * 60 * 1000 * 1000000,
   645            IgnoreSystemJobs: false,
   646          },
   647        },
   648        'Drain with deadline toggled'
   649      );
   650  
   651      await ClientDetail.drainPopover.toggle();
   652      await ClientDetail.drainPopover.deadlineOptions.open();
   653      await ClientDetail.drainPopover.deadlineOptions.options[1].choose();
   654      await ClientDetail.drainPopover.submit();
   655  
   656      request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
   657  
   658      assert.deepEqual(
   659        JSON.parse(request.requestBody),
   660        {
   661          NodeID: node.id,
   662          DrainSpec: {
   663            Deadline: 4 * 60 * 60 * 1000 * 1000000,
   664            IgnoreSystemJobs: false,
   665          },
   666        },
   667        'Drain with non-default preset deadline set'
   668      );
   669  
   670      await ClientDetail.drainPopover.toggle();
   671      await ClientDetail.drainPopover.deadlineOptions.open();
   672      const optionsCount = ClientDetail.drainPopover.deadlineOptions.options.length;
   673      await ClientDetail.drainPopover.deadlineOptions.options.objectAt(optionsCount - 1).choose();
   674      await ClientDetail.drainPopover.setCustomDeadline('1h40m20s');
   675      await ClientDetail.drainPopover.submit();
   676  
   677      request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
   678  
   679      assert.deepEqual(
   680        JSON.parse(request.requestBody),
   681        {
   682          NodeID: node.id,
   683          DrainSpec: {
   684            Deadline: ((1 * 60 + 40) * 60 + 20) * 1000 * 1000000,
   685            IgnoreSystemJobs: false,
   686          },
   687        },
   688        'Drain with custom deadline set'
   689      );
   690  
   691      await ClientDetail.drainPopover.toggle();
   692      await ClientDetail.drainPopover.deadlineToggle.toggle();
   693      await ClientDetail.drainPopover.forceDrainToggle.toggle();
   694      await ClientDetail.drainPopover.submit();
   695  
   696      request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
   697  
   698      assert.deepEqual(
   699        JSON.parse(request.requestBody),
   700        {
   701          NodeID: node.id,
   702          DrainSpec: {
   703            Deadline: -1,
   704            IgnoreSystemJobs: false,
   705          },
   706        },
   707        'Drain with force set'
   708      );
   709  
   710      await ClientDetail.drainPopover.toggle();
   711      await ClientDetail.drainPopover.systemJobsToggle.toggle();
   712      await ClientDetail.drainPopover.submit();
   713  
   714      request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
   715  
   716      assert.deepEqual(
   717        JSON.parse(request.requestBody),
   718        {
   719          NodeID: node.id,
   720          DrainSpec: {
   721            Deadline: -1,
   722            IgnoreSystemJobs: true,
   723          },
   724        },
   725        'Drain system jobs unset'
   726      );
   727    });
   728  
   729    test('the drain popover cancel button closes the popover', async function(assert) {
   730      node = server.create('node', {
   731        drain: false,
   732        schedulingEligibility: 'eligible',
   733      });
   734  
   735      await ClientDetail.visit({ id: node.id });
   736      assert.notOk(ClientDetail.drainPopover.isOpen);
   737  
   738      await ClientDetail.drainPopover.toggle();
   739      assert.ok(ClientDetail.drainPopover.isOpen);
   740  
   741      await ClientDetail.drainPopover.cancel();
   742      assert.notOk(ClientDetail.drainPopover.isOpen);
   743      assert.equal(server.pretender.handledRequests.filterBy('method', 'POST'), 0);
   744    });
   745  
   746    test('toggling eligibility is disabled while a drain is active', async function(assert) {
   747      node = server.create('node', {
   748        drain: true,
   749        schedulingEligibility: 'ineligible',
   750      });
   751  
   752      await ClientDetail.visit({ id: node.id });
   753      assert.ok(ClientDetail.eligibilityToggle.isDisabled);
   754    });
   755  
   756    test('stopping a drain sends the correct POST request', async function(assert) {
   757      node = server.create('node', {
   758        drain: true,
   759        schedulingEligibility: 'ineligible',
   760      });
   761  
   762      await ClientDetail.visit({ id: node.id });
   763      assert.ok(ClientDetail.stopDrainIsPresent);
   764  
   765      await ClientDetail.stopDrain.idle();
   766      await ClientDetail.stopDrain.confirm();
   767  
   768      const request = server.pretender.handledRequests.findBy('method', 'POST');
   769      assert.equal(request.url, `/v1/node/${node.id}/drain`);
   770      assert.deepEqual(JSON.parse(request.requestBody), {
   771        NodeID: node.id,
   772        DrainSpec: null,
   773      });
   774    });
   775  
   776    test('when a drain is active, the "drain" popover is labeled as the "update" popover', async function(assert) {
   777      node = server.create('node', {
   778        drain: true,
   779        schedulingEligibility: 'ineligible',
   780      });
   781  
   782      await ClientDetail.visit({ id: node.id });
   783      assert.equal(ClientDetail.drainPopover.label, 'Update Drain');
   784    });
   785  
   786    test('forcing a drain sends the correct POST request', async function(assert) {
   787      node = server.create('node', {
   788        drain: true,
   789        schedulingEligibility: 'ineligible',
   790        drainStrategy: {
   791          Deadline: 0,
   792          IgnoreSystemJobs: true,
   793        },
   794      });
   795  
   796      await ClientDetail.visit({ id: node.id });
   797      await ClientDetail.drainDetails.force.idle();
   798      await ClientDetail.drainDetails.force.confirm();
   799  
   800      const request = server.pretender.handledRequests.findBy('method', 'POST');
   801      assert.equal(request.url, `/v1/node/${node.id}/drain`);
   802      assert.deepEqual(JSON.parse(request.requestBody), {
   803        NodeID: node.id,
   804        DrainSpec: {
   805          Deadline: -1,
   806          IgnoreSystemJobs: true,
   807        },
   808      });
   809    });
   810  
   811    test('when stopping a drain fails, an error is shown', async function(assert) {
   812      node = server.create('node', {
   813        drain: true,
   814        schedulingEligibility: 'ineligible',
   815      });
   816  
   817      server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
   818  
   819      await ClientDetail.visit({ id: node.id });
   820      await ClientDetail.stopDrain.idle();
   821      await ClientDetail.stopDrain.confirm();
   822  
   823      assert.ok(ClientDetail.stopDrainError.isPresent);
   824      assert.ok(ClientDetail.stopDrainError.title.includes('Stop Drain Error'));
   825  
   826      await ClientDetail.stopDrainError.dismiss();
   827      assert.notOk(ClientDetail.stopDrainError.isPresent);
   828    });
   829  
   830    test('when starting a drain fails, an error message is shown', async function(assert) {
   831      node = server.create('node', {
   832        drain: false,
   833        schedulingEligibility: 'eligible',
   834      });
   835  
   836      server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
   837  
   838      await ClientDetail.visit({ id: node.id });
   839      await ClientDetail.drainPopover.toggle();
   840      await ClientDetail.drainPopover.submit();
   841  
   842      assert.ok(ClientDetail.drainError.isPresent);
   843      assert.ok(ClientDetail.drainError.title.includes('Drain Error'));
   844  
   845      await ClientDetail.drainError.dismiss();
   846      assert.notOk(ClientDetail.drainError.isPresent);
   847    });
   848  
   849    test('when updating a drain fails, an error message is shown', async function(assert) {
   850      node = server.create('node', {
   851        drain: true,
   852        schedulingEligibility: 'ineligible',
   853      });
   854  
   855      server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
   856  
   857      await ClientDetail.visit({ id: node.id });
   858      await ClientDetail.drainPopover.toggle();
   859      await ClientDetail.drainPopover.submit();
   860  
   861      assert.ok(ClientDetail.drainError.isPresent);
   862      assert.ok(ClientDetail.drainError.title.includes('Drain Error'));
   863  
   864      await ClientDetail.drainError.dismiss();
   865      assert.notOk(ClientDetail.drainError.isPresent);
   866    });
   867  
   868    test('when toggling eligibility fails, an error message is shown', async function(assert) {
   869      node = server.create('node', {
   870        drain: false,
   871        schedulingEligibility: 'eligible',
   872      });
   873  
   874      server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']);
   875  
   876      await ClientDetail.visit({ id: node.id });
   877      await ClientDetail.eligibilityToggle.toggle();
   878  
   879      assert.ok(ClientDetail.eligibilityError.isPresent);
   880      assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error'));
   881  
   882      await ClientDetail.eligibilityError.dismiss();
   883      assert.notOk(ClientDetail.eligibilityError.isPresent);
   884    });
   885  
   886    test('when navigating away from a client that has an error message to another client, the error is not shown', async function(assert) {
   887      node = server.create('node', {
   888        drain: false,
   889        schedulingEligibility: 'eligible',
   890      });
   891  
   892      const node2 = server.create('node');
   893  
   894      server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']);
   895  
   896      await ClientDetail.visit({ id: node.id });
   897      await ClientDetail.eligibilityToggle.toggle();
   898  
   899      assert.ok(ClientDetail.eligibilityError.isPresent);
   900      assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error'));
   901  
   902      await ClientDetail.visit({ id: node2.id });
   903  
   904      assert.notOk(ClientDetail.eligibilityError.isPresent);
   905    });
   906  
   907    test('toggling eligibility and node drain are disabled when the active ACL token does not permit node write', async function(assert) {
   908      window.localStorage.nomadTokenSecret = clientToken.secretId;
   909  
   910      await ClientDetail.visit({ id: node.id });
   911      assert.ok(ClientDetail.eligibilityToggle.isDisabled);
   912      assert.ok(ClientDetail.drainPopover.isDisabled);
   913    });
   914  
   915    test('the host volumes table lists all host volumes in alphabetical order by name', async function(assert) {
   916      await ClientDetail.visit({ id: node.id });
   917  
   918      const sortedHostVolumes = Object.keys(node.hostVolumes)
   919        .map(key => node.hostVolumes[key])
   920        .sortBy('Name');
   921  
   922      assert.ok(ClientDetail.hasHostVolumes);
   923      assert.equal(ClientDetail.hostVolumes.length, Object.keys(node.hostVolumes).length);
   924  
   925      ClientDetail.hostVolumes.forEach((volume, index) => {
   926        assert.equal(volume.name, sortedHostVolumes[index].Name);
   927      });
   928    });
   929  
   930    test('each host volume row contains information about the host volume', async function(assert) {
   931      await ClientDetail.visit({ id: node.id });
   932  
   933      const sortedHostVolumes = Object.keys(node.hostVolumes)
   934        .map(key => node.hostVolumes[key])
   935        .sortBy('Name');
   936  
   937      ClientDetail.hostVolumes[0].as(volume => {
   938        const volumeRow = sortedHostVolumes[0];
   939        assert.equal(volume.name, volumeRow.Name);
   940        assert.equal(volume.path, volumeRow.Path);
   941        assert.equal(volume.permissions, volumeRow.ReadOnly ? 'Read' : 'Read/Write');
   942      });
   943    });
   944  
   945    test('the host volumes table is not shown if the client has no host volumes', async function(assert) {
   946      node = server.create('node', 'noHostVolumes');
   947  
   948      await ClientDetail.visit({ id: node.id });
   949  
   950      assert.notOk(ClientDetail.hasHostVolumes);
   951    });
   952  });
   953  
   954  module('Acceptance | client detail (multi-namespace)', function(hooks) {
   955    setupApplicationTest(hooks);
   956    setupMirage(hooks);
   957  
   958    hooks.beforeEach(function() {
   959      server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
   960      node = server.db.nodes[0];
   961  
   962      // Related models
   963      server.create('namespace');
   964      server.create('namespace', { id: 'other-namespace' });
   965  
   966      server.create('agent');
   967  
   968      // Make a job for each namespace, but have both scheduled on the same node
   969      server.create('job', { id: 'job-1', namespaceId: 'default', createAllocations: false });
   970      server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' });
   971  
   972      server.create('job', { id: 'job-2', namespaceId: 'other-namespace', createAllocations: false });
   973      server.createList('allocation', 3, {
   974        nodeId: node.id,
   975        jobId: 'job-2',
   976        clientStatus: 'running',
   977      });
   978    });
   979  
   980    test('when the node has allocations on different namespaces, the associated jobs are fetched correctly', async function(assert) {
   981      window.localStorage.nomadActiveNamespace = 'other-namespace';
   982  
   983      await ClientDetail.visit({ id: node.id });
   984  
   985      assert.equal(
   986        ClientDetail.allocations.length,
   987        server.db.allocations.length,
   988        'All allocations are scheduled on this node'
   989      );
   990      assert.ok(
   991        server.pretender.handledRequests.findBy('url', '/v1/job/job-1'),
   992        'Job One fetched correctly'
   993      );
   994      assert.ok(
   995        server.pretender.handledRequests.findBy('url', '/v1/job/job-2?namespace=other-namespace'),
   996        'Job Two fetched correctly'
   997      );
   998    });
   999  });