github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/ui/tests/acceptance/client-detail-test.js (about)

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