github.com/hernad/nomad@v1.6.112/ui/app/templates/clients/client/index.hbs (about)

     1  {{!
     2    Copyright (c) HashiCorp, Inc.
     3    SPDX-License-Identifier: MPL-2.0
     4  ~}}
     5  
     6  {{page-title "Client " (or this.model.name this.model.shortId)}}
     7  <ClientSubnav @client={{this.model}} />
     8  <section class="section">
     9    {{#if this.eligibilityError}}
    10      <div data-test-eligibility-error class="columns">
    11        <div class="column">
    12          <div class="notification is-danger">
    13            <h3 data-test-title class="title is-4">
    14              Eligibility Error
    15            </h3>
    16            <p data-test-message>
    17              {{this.eligibilityError}}
    18            </p>
    19          </div>
    20        </div>
    21        <div class="column is-centered is-minimum">
    22          <button
    23            data-test-dismiss
    24            class="button is-danger"
    25            onclick={{action (mut this.eligibilityError) ""}}
    26            type="button"
    27          >
    28            Okay
    29          </button>
    30        </div>
    31      </div>
    32    {{/if}}
    33    {{#if this.stopDrainError}}
    34      <div data-test-stop-drain-error class="columns">
    35        <div class="column">
    36          <div class="notification is-danger">
    37            <h3 data-test-title class="title is-4">
    38              Stop Drain Error
    39            </h3>
    40            <p data-test-message>
    41              {{this.stopDrainError}}
    42            </p>
    43          </div>
    44        </div>
    45        <div class="column is-centered is-minimum">
    46          <button
    47            data-test-dismiss
    48            class="button is-danger"
    49            onclick={{action (mut this.stopDrainError) ""}}
    50            type="button"
    51          >
    52            Okay
    53          </button>
    54        </div>
    55      </div>
    56    {{/if}}
    57    {{#if this.drainError}}
    58      <div data-test-drain-error class="columns">
    59        <div class="column">
    60          <div class="notification is-danger">
    61            <h3 data-test-title class="title is-4">
    62              Drain Error
    63            </h3>
    64            <p data-test-message>
    65              {{this.drainError}}
    66            </p>
    67          </div>
    68        </div>
    69        <div class="column is-centered is-minimum">
    70          <button
    71            data-test-dismiss
    72            class="button is-danger"
    73            onclick={{action (mut this.drainError) ""}}
    74            type="button"
    75          >
    76            Okay
    77          </button>
    78        </div>
    79      </div>
    80    {{/if}}
    81    {{#if this.showDrainStoppedNotification}}
    82      <div class="notification is-info">
    83        <div data-test-drain-stopped-notification class="columns">
    84          <div class="column">
    85            <h3 data-test-title class="title is-4">
    86              Drain Stopped
    87            </h3>
    88            <p data-test-message>
    89              The drain has been stopped and the node has been set to ineligible.
    90            </p>
    91          </div>
    92          <div class="column is-centered is-minimum">
    93            <button
    94              data-test-dismiss
    95              class="button is-info"
    96              onclick={{action (mut this.showDrainStoppedNotification) false}}
    97              type="button"
    98            >
    99              Okay
   100            </button>
   101          </div>
   102        </div>
   103      </div>
   104    {{/if}}
   105    {{#if this.showDrainUpdateNotification}}
   106      <div class="notification is-info">
   107        <div data-test-drain-updated-notification class="columns">
   108          <div class="column">
   109            <h3 data-test-title class="title is-4">
   110              Drain Updated
   111            </h3>
   112            <p data-test-message>
   113              The new drain specification has been applied.
   114            </p>
   115          </div>
   116          <div class="column is-centered is-minimum">
   117            <button
   118              data-test-dismiss
   119              class="button is-info"
   120              onclick={{action (mut this.showDrainUpdateNotification) false}}
   121              type="button"
   122            >
   123              Okay
   124            </button>
   125          </div>
   126        </div>
   127      </div>
   128    {{/if}}
   129    {{#if this.showDrainNotification}}
   130      <div class="notification is-info">
   131        <div data-test-drain-complete-notification class="columns">
   132          <div class="column">
   133            <h3 data-test-title class="title is-4">
   134              Drain Complete
   135            </h3>
   136            <p data-test-message>
   137              Allocations have been drained and the node has been set to ineligible.
   138            </p>
   139          </div>
   140          <div class="column is-centered is-minimum">
   141            <button
   142              data-test-dimiss
   143              class="button is-info"
   144              onclick={{action (mut this.showDrainNotification) false}}
   145              type="button"
   146            >
   147              Okay
   148            </button>
   149          </div>
   150        </div>
   151      </div>
   152    {{/if}}
   153    <div class="toolbar">
   154      <div class="toolbar-item is-top-aligned is-minimum">
   155        <span class="title">
   156          <span
   157            data-test-node-status="{{this.model.compositeStatus}}"
   158            class="node-status-light {{this.model.compositeStatus}}"
   159          >
   160            {{x-icon this.model.compositeStatusIcon}}
   161          </span>
   162        </span>
   163      </div>
   164      <div class="toolbar-item">
   165        <h1 data-test-title class="title with-subheading">
   166          {{or this.model.name this.model.shortId}}
   167        </h1>
   168        <p>
   169          <label class="is-interactive">
   170            <Toggle
   171              data-test-eligibility-toggle
   172              @isActive={{this.model.isEligible}}
   173              @isDisabled={{or
   174                this.setEligibility.isRunning
   175                this.model.isDraining
   176                (cannot "write client")
   177              }}
   178              @onToggle={{perform this.setEligibility (not this.model.isEligible)
   179              }}
   180            >
   181              Eligible
   182            </Toggle>
   183            <span
   184              class="tooltip"
   185              aria-label="Only eligible clients can receive allocations"
   186            >
   187              {{x-icon "info-circle-outline" class="is-faded"}}
   188            </span>
   189          </label>
   190          <span
   191            data-test-node-id
   192            class="tag is-hollow is-small no-text-transform"
   193          >
   194            {{this.model.id}}
   195            <CopyButton @clipboardText={{this.model.id}} @compact={{true}} @inset={{true}} />
   196          </span>
   197        </p>
   198      </div>
   199      <div class="toolbar-item is-right-aligned is-top-aligned">
   200        {{#if this.model.isDraining}}
   201          <TwoStepButton
   202            data-test-drain-stop
   203            @idleText="Stop Drain"
   204            @cancelText="Cancel"
   205            @confirmText="Yes, Stop Drain"
   206            @confirmationMessage="Are you sure you want to stop this drain?"
   207            @awaitingConfirmation={{this.stopDrain.isRunning}}
   208            @onConfirm={{perform this.stopDrain}}
   209          />
   210        {{/if}}
   211      </div>
   212      <div class="toolbar-item is-right-aligned is-top-aligned">
   213        <DrainPopover
   214          @client={{this.model}}
   215          @isDisabled={{cannot "write client"}}
   216          @onDrain={{action "drainNotify"}}
   217          @onError={{action "setDrainError"}}
   218        />
   219      </div>
   220    </div>
   221    <div class="boxed-section is-small">
   222      <div class="boxed-section-body inline-definitions">
   223        <span class="label">
   224          Client Details
   225        </span>
   226        <span class="pair" data-test-status-definition>
   227          <span class="term">
   228            Status
   229          </span>
   230          <span class="status-text node-{{this.model.status}}">
   231            {{this.model.status}}
   232          </span>
   233        </span>
   234        <span class="pair" data-test-address-definition>
   235          <span class="term">
   236            Address
   237          </span>
   238          {{this.model.httpAddr}}
   239        </span>
   240        <span class="pair" data-test-datacenter-definition>
   241          <span class="term">
   242            Datacenter
   243          </span>
   244          {{this.model.datacenter}}
   245        </span>
   246        <span class="pair" data-test-node-pool>
   247          <span class="term">
   248            Node Pool
   249          </span>
   250          {{#if this.model.nodePool}}{{this.model.nodePool}}{{else}}-{{/if}}
   251        </span>
   252        {{#if this.model.nodeClass}}
   253          <span class="pair" data-test-node-class>
   254            <span class="term">
   255              Class
   256            </span>
   257            {{this.model.nodeClass}}
   258          </span>
   259        {{/if}}
   260        <span class="pair" data-test-driver-health>
   261          <span class="term">
   262            Drivers
   263          </span>
   264          {{#if this.model.unhealthyDrivers.length}}
   265            {{x-icon "alert-triangle" class="is-text is-warning"}}
   266            {{this.model.unhealthyDrivers.length}}
   267            of
   268            {{this.model.detectedDrivers.length}}
   269            {{pluralize "driver" this.model.detectedDrivers.length}}
   270            unhealthy
   271          {{else}}
   272            All healthy
   273          {{/if}}
   274        </span>
   275      </div>
   276    </div>
   277    {{#if this.model.drainStrategy}}
   278      <div data-test-drain-details class="boxed-section is-info">
   279        <div class="boxed-section-head">
   280          <div class="boxed-section-row">
   281            Drain Strategy
   282          </div>
   283          <div class="boxed-section-row">
   284            <div class="inline-definitions is-small">
   285              {{#unless this.model.drainStrategy.hasNoDeadline}}
   286                <span class="pair">
   287                  <span class="term">
   288                    Duration
   289                  </span>
   290                  {{#if this.model.drainStrategy.isForced}}
   291                    <span data-test-duration>
   292                      --
   293                    </span>
   294                  {{else}}
   295                    <span
   296                      data-test-duration
   297                      class="tooltip"
   298                      aria-label={{format-duration
   299                        this.model.drainStrategy.deadline
   300                      }}
   301                    >
   302                      {{format-duration this.model.drainStrategy.deadline}}
   303                    </span>
   304                  {{/if}}
   305                </span>
   306              {{/unless}}
   307              <span class="pair">
   308                <span class="term">
   309                  {{if
   310                    this.model.drainStrategy.hasNoDeadline
   311                    "Deadline"
   312                    "Remaining"
   313                  }}
   314                </span>
   315                {{#if this.model.drainStrategy.hasNoDeadline}}
   316                  <span data-test-deadline>
   317                    No deadline
   318                  </span>
   319                {{else if this.model.drainStrategy.isForced}}
   320                  <span data-test-deadline>
   321                    --
   322                  </span>
   323                {{else}}
   324                  <span
   325                    data-test-deadline
   326                    class="tooltip"
   327                    aria-label={{format-ts this.model.drainStrategy.forceDeadline
   328                    }}
   329                  >
   330                    {{moment-from-now
   331                      this.model.drainStrategy.forceDeadline
   332                      interval=1000
   333                      hideAffix=true
   334                    }}
   335                  </span>
   336                {{/if}}
   337              </span>
   338              <span data-test-force-drain-text class="pair">
   339                <span class="term">
   340                  Force Drain
   341                </span>
   342                {{#if this.model.drainStrategy.isForced}}
   343                  {{x-icon "alert-triangle" class="is-text is-warning"}}Yes
   344                {{else}}
   345                  No
   346                {{/if}}
   347              </span>
   348              <span data-test-drain-system-jobs-text class="pair">
   349                <span class="term">
   350                  Drain System Jobs
   351                </span>
   352                {{if this.model.drainStrategy.ignoreSystemJobs "No" "Yes"}}
   353              </span>
   354            </div>
   355            {{#unless this.model.drainStrategy.isForced}}
   356              <div class="pull-right">
   357                <TwoStepButton
   358                  data-test-force
   359                  @alignRight={{true}}
   360                  @classes={{hash
   361                    idleButton="is-warning"
   362                    confirmationMessage="inherit-color"
   363                    cancelButton="is-danger is-important"
   364                    confirmButton="is-warning"
   365                  }}
   366                  @idleText="Force Drain"
   367                  @cancelText="Cancel"
   368                  @confirmText="Yes, Force Drain"
   369                  @confirmationMessage="Are you sure you want to force drain?"
   370                  @awaitingConfirmation={{this.forceDrain.isRunning}}
   371                  @onConfirm={{perform this.forceDrain}}
   372                />
   373              </div>
   374            {{/unless}}
   375          </div>
   376        </div>
   377        <div class="boxed-section-body">
   378          <div class="columns">
   379            <div class="column nowrap is-minimum">
   380              <div class="metric-group">
   381                <div class="metric is-primary">
   382                  <h3 class="label">
   383                    Complete
   384                  </h3>
   385                  <p data-test-complete-count class="value">
   386                    {{this.model.completeAllocations.length}}
   387                  </p>
   388                </div>
   389              </div>
   390              <div class="metric-group">
   391                <div class="metric">
   392                  <h3 class="label">
   393                    Migrating
   394                  </h3>
   395                  <p data-test-migrating-count class="value">
   396                    {{this.model.migratingAllocations.length}}
   397                  </p>
   398                </div>
   399              </div>
   400              <div class="metric-group">
   401                <div class="metric">
   402                  <h3 class="label">
   403                    Remaining
   404                  </h3>
   405                  <p data-test-remaining-count class="value">
   406                    {{this.model.runningAllocations.length}}
   407                  </p>
   408                </div>
   409              </div>
   410            </div>
   411            <div class="column">
   412              <h3 class="title is-4">
   413                Status
   414              </h3>
   415              {{#if this.model.lastMigrateTime}}
   416                <p data-test-status>
   417                  {{moment-to-now
   418                    this.model.lastMigrateTime
   419                    interval=1000
   420                    hideAffix=true
   421                  }}
   422                  since an allocation was successfully migrated.
   423                </p>
   424              {{else}}
   425                <p data-test-status>
   426                  No allocations migrated.
   427                </p>
   428              {{/if}}
   429            </div>
   430          </div>
   431        </div>
   432      </div>
   433    {{/if}}
   434    <div class="boxed-section">
   435      <div class="boxed-section-head is-hollow">
   436        Host Resource Utilization
   437        <span
   438          class="tooltip multiline pad-left"
   439          aria-label="All allocation and system processes aggregated"
   440        >
   441          {{x-icon "info-circle-outline" class="is-faded"}}
   442        </span>
   443      </div>
   444      <div class="boxed-section-body">
   445        <div class="columns">
   446          <div class="column">
   447            <PrimaryMetric::Node @node={{this.model}} @metric="cpu" />
   448          </div>
   449          <div class="column">
   450            <PrimaryMetric::Node @node={{this.model}} @metric="memory" />
   451          </div>
   452        </div>
   453      </div>
   454    </div>
   455    <div class="boxed-section">
   456      <div class="boxed-section-head">
   457        <div>
   458          Allocations
   459          <button
   460            role="button"
   461            class="badge is-white"
   462            onclick={{action "setPreemptionFilter" false}}
   463            data-test-filter-all
   464            type="button"
   465          >
   466            {{this.model.allocations.length}}
   467          </button>
   468          {{#if this.preemptions.length}}
   469            <button
   470              role="button"
   471              class="badge is-warning"
   472              onclick={{action "setPreemptionFilter" true}}
   473              data-test-filter-preemptions
   474              type="button"
   475            >
   476              {{this.preemptions.length}}
   477              {{pluralize "preemption" this.preemptions.length}}
   478            </button>
   479          {{/if}}
   480        </div>
   481        <div class="pull-right is-subsection">
   482          <MultiSelectDropdown
   483            data-test-allocation-namespace-facet
   484            @label="Namespace"
   485            @options={{this.optionsNamespace}}
   486            @selection={{this.selectionNamespace}}
   487            @onSelect={{action this.setFacetQueryParam "qpNamespace"}}
   488          />
   489          <MultiSelectDropdown
   490            data-test-allocation-job-facet
   491            @label="Job"
   492            @options={{this.optionsJob}}
   493            @selection={{this.selectionJob}}
   494            @onSelect={{action this.setFacetQueryParam "qpJob"}}
   495          />
   496          <MultiSelectDropdown
   497            data-test-allocation-status-facet
   498            @label="Status"
   499            @options={{this.optionsAllocationStatus}}
   500            @selection={{this.selectionStatus}}
   501            @onSelect={{action this.setFacetQueryParam "qpStatus"}}
   502          />
   503          <SearchBox
   504            @searchTerm={{mut this.searchTerm}}
   505            @onChange={{action this.resetPagination}}
   506            @placeholder="Search allocations..."
   507            @inputClass="is-compact"
   508            @class="is-padded"
   509          />
   510  
   511          <span class="is-padded is-one-line">
   512            <Toggle
   513              @isActive={{this.showSubTasks}}
   514              @onToggle={{this.toggleShowSubTasks}}
   515              title="Show tasks of allocations"
   516            >
   517              Show Tasks
   518            </Toggle>
   519          </span>
   520        </div>
   521      </div>
   522      <div
   523        class="boxed-section-body
   524          {{if this.sortedAllocations.length "is-full-bleed"}}"
   525      >
   526        {{#if this.sortedAllocations.length}}
   527          <ListPagination
   528            @source={{this.sortedAllocations}}
   529            @size={{this.pageSize}}
   530            @page={{this.currentPage}} as |p|
   531          >
   532            <ListTable
   533              @source={{p.list}}
   534              @sortProperty={{this.sortProperty}}
   535              @sortDescending={{this.sortDescending}}
   536              @class="with-foot {{if this.showSubTasks "with-collapsed-borders"}}" as |t|
   537            >
   538              <t.head>
   539                <th class="is-narrow"></th>
   540                <t.sort-by @prop="shortId">
   541                  ID
   542                </t.sort-by>
   543                <t.sort-by @prop="createIndex" @title="Create Index">
   544                  Created
   545                </t.sort-by>
   546                <t.sort-by @prop="modifyIndex" @title="Modify Index">
   547                  Modified
   548                </t.sort-by>
   549                <t.sort-by @prop="statusIndex">
   550                  Status
   551                </t.sort-by>
   552                <t.sort-by @prop="job.name">
   553                  Job
   554                </t.sort-by>
   555                <t.sort-by @prop="jobVersion">
   556                  Version
   557                </t.sort-by>
   558                <th>
   559                  Volume
   560                </th>
   561                <th>
   562                  CPU
   563                </th>
   564                <th>
   565                  Memory
   566                </th>
   567              </t.head>
   568              <t.body as |row|>
   569                <AllocationRow
   570                  {{keyboard-shortcut
   571                    enumerated=true
   572                    action=(action "gotoAllocation" row.model)
   573                  }}
   574                  @allocation={{row.model}}
   575                  @context="node"
   576                  @onClick={{action "gotoAllocation" row.model}}
   577                  @data-test-allocation={{row.model.id}}
   578                />
   579                {{#if this.showSubTasks}}
   580                  {{#each row.model.states as |task|}}
   581                    <TaskSubRow @namespan="8" @taskState={{task}} @active={{eq this.activeTask (concat task.allocation.id "-" task.name)}} @onSetActiveTask={{action 'setActiveTaskQueryParam'}} />
   582                  {{/each}}
   583                {{/if}}
   584              </t.body>
   585            </ListTable>
   586            <div class="table-foot">
   587              <nav class="pagination">
   588                <div class="pagination-numbers">
   589                  {{p.startsAt}}
   590                  –
   591                  {{p.endsAt}}
   592                  of
   593                  {{this.sortedAllocations.length}}
   594                </div>
   595                <p.prev @class="pagination-previous">
   596                  &lt;
   597                </p.prev>
   598                <p.next @class="pagination-next">
   599                  >
   600                </p.next>
   601                <ul class="pagination-list"></ul>
   602              </nav>
   603            </div>
   604          </ListPagination>
   605        {{else}}
   606          <div data-test-empty-allocations-list class="empty-message">
   607            {{#if (eq this.visibleAllocations.length 0)}}
   608              <h3
   609                data-test-empty-allocations-list-headline
   610                class="empty-message-headline"
   611              >
   612                No Allocations
   613              </h3>
   614              <p data-test-empty-allocations-list-body class="empty-message-body">
   615                The node doesn't have any allocations.
   616              </p>
   617            {{else if this.searchTerm}}
   618              <h3
   619                data-test-empty-allocations-list-headline
   620                class="empty-message-headline"
   621              >
   622                No Matches
   623              </h3>
   624              <p class="empty-message-body">
   625                No allocations match the term
   626                <strong>
   627                  {{this.searchTerm}}
   628                </strong>
   629              </p>
   630            {{else if (eq this.sortedAllocations.length 0)}}
   631              <h3
   632                data-test-empty-allocations-list-headline
   633                class="empty-message-headline"
   634              >
   635                No Matches
   636              </h3>
   637              <p class="empty-message-body">
   638                No allocations match your current filter selection.
   639              </p>
   640            {{/if}}
   641          </div>
   642        {{/if}}
   643      </div>
   644    </div>
   645    <div data-test-client-events class="boxed-section">
   646      <div class="boxed-section-head">
   647        Client Events
   648      </div>
   649      <div class="boxed-section-body is-full-bleed">
   650        <ListTable @source={{this.sortedEvents}} @class="is-striped" as |t|>
   651          <t.head>
   652            <th class="is-2">
   653              Time
   654            </th>
   655            <th class="is-2">
   656              Subsystem
   657            </th>
   658            <th>
   659              Message
   660            </th>
   661          </t.head>
   662          <t.body as |row|>
   663            <tr data-test-client-event>
   664              <td data-test-client-event-time>
   665                {{format-ts row.model.time}}
   666              </td>
   667              <td data-test-client-event-subsystem>
   668                {{row.model.subsystem}}
   669              </td>
   670              <td data-test-client-event-message>
   671                {{#if row.model.message}}
   672                  {{#if row.model.driver}}
   673                    <span class="badge is-secondary is-small">
   674                      {{row.model.driver}}
   675                    </span>
   676                  {{/if}}
   677                  {{row.model.message}}
   678                {{else}}
   679                  <em>
   680                    No message
   681                  </em>
   682                {{/if}}
   683              </td>
   684            </tr>
   685          </t.body>
   686        </ListTable>
   687      </div>
   688    </div>
   689    {{#if this.sortedHostVolumes.length}}
   690      <div data-test-client-host-volumes class="boxed-section">
   691        <div class="boxed-section-head">
   692          Host Volumes
   693        </div>
   694        <div class="boxed-section-body is-full-bleed">
   695          <ListTable
   696            @source={{this.sortedHostVolumes}}
   697            @class="is-striped" as |t|
   698          >
   699            <t.head>
   700              <th>
   701                Name
   702              </th>
   703              <th>
   704                Source
   705              </th>
   706              <th>
   707                Permissions
   708              </th>
   709            </t.head>
   710            <t.body as |row|>
   711              <tr data-test-client-host-volume>
   712                <td data-test-name>
   713                  {{row.model.name}}
   714                </td>
   715                <td data-test-path>
   716                  <code>
   717                    {{row.model.path}}
   718                  </code>
   719                </td>
   720                <td data-test-permissions>
   721                  {{if row.model.readOnly "Read" "Read/Write"}}
   722                </td>
   723              </tr>
   724            </t.body>
   725          </ListTable>
   726        </div>
   727      </div>
   728    {{/if}}
   729    <div data-test-driver-status class="boxed-section">
   730      <div class="boxed-section-head">
   731        Driver Status
   732      </div>
   733      <div class="boxed-section-body">
   734        <ListAccordion @source={{this.sortedDrivers}} @key="name" as |a|>
   735          <a.head
   736            @buttonLabel="details"
   737            @buttonType={{"client-detail"}}
   738            @isExpandable={{a.item.detected}}
   739          >
   740            <div
   741              class="columns inline-definitions
   742                {{unless a.item.detected "is-faded"}}"
   743            >
   744              <div class="column is-1">
   745                <span data-test-name>
   746                  {{a.item.name}}
   747                </span>
   748              </div>
   749              <div class="column is-2">
   750                {{#if a.item.detected}}
   751                  <span data-test-health>
   752                    <span class="color-swatch {{a.item.healthClass}}"></span>
   753                    {{if a.item.healthy "Healthy" "Unhealthy"}}
   754                  </span>
   755                {{/if}}
   756              </div>
   757              <div class="column">
   758                <span class="pair">
   759                  <span class="term">
   760                    Detected
   761                  </span>
   762                  <span data-test-detected>
   763                    {{if a.item.detected "Yes" "No"}}
   764                  </span>
   765                </span>
   766                <span class="is-pulled-right">
   767                  <span class="pair">
   768                    <span class="term">
   769                      Last Updated
   770                    </span>
   771                    <span
   772                      data-test-last-updated
   773                      class="tooltip"
   774                      aria-label="{{format-ts a.item.updateTime}}"
   775                    >
   776                      {{moment-from-now a.item.updateTime interval=1000}}
   777                    </span>
   778                  </span>
   779                </span>
   780              </div>
   781            </div>
   782          </a.head>
   783          <a.body>
   784            <p data-test-health-description class="message">
   785              {{a.item.healthDescription}}
   786            </p>
   787            <div data-test-driver-attributes class="boxed-section">
   788              <div class="boxed-section-head">
   789                {{capitalize a.item.name}}
   790                Attributes
   791              </div>
   792              {{#if a.item.attributes.structured}}
   793                <div class="boxed-section-body is-full-bleed">
   794                  <AttributesTable
   795                    @attributePairs={{a.item.attributesShort}}
   796                    @class="attributes-table"
   797                  />
   798                </div>
   799              {{else}}
   800                <div class="boxed-section-body">
   801                  <div class="empty-message">
   802                    <h3 class="empty-message-headline">
   803                      No Driver Attributes
   804                    </h3>
   805                  </div>
   806                </div>
   807              {{/if}}
   808            </div>
   809          </a.body>
   810        </ListAccordion>
   811      </div>
   812    </div>
   813    <div class="boxed-section">
   814      <div class="boxed-section-head">
   815        Attributes
   816      </div>
   817      <div class="boxed-section-body is-full-bleed">
   818        <AttributesTable
   819          data-test-attributes
   820          @attributePairs={{this.model.attributes.structured}}
   821          @class="attributes-table"
   822          @copyable={{true}}
   823        />
   824      </div>
   825    </div>
   826    <div class="boxed-section">
   827      <div class="boxed-section-head">
   828        Meta
   829      </div>
   830      {{#if this.hasMeta}}
   831        <div class="boxed-section-body is-full-bleed">
   832          <AttributesTable
   833            data-test-meta
   834            @attributePairs={{this.model.meta.structured}}
   835            @editable={{can "write client"}}
   836            @onKVSave={{this.addDynamicMetaData}}
   837            @onKVEdit={{this.validateMetadata}}
   838            @class="attributes-table"
   839          />
   840        </div>
   841      {{else}}
   842        <div class="boxed-section-body">
   843          <div data-test-empty-meta-message class="empty-message">
   844            <h3 class="empty-message-headline">
   845              No Meta Attributes
   846            </h3>
   847            <p class="empty-message-body">
   848              This client is configured with no meta attributes.
   849            </p>
   850          </div>
   851        </div>
   852      {{/if}}
   853      {{#if (can "write client")}}
   854        {{#if this.editingMetadata}}
   855          <div class="add-dynamic-metadata">
   856            <h3 class="title is-6">Add Dynamic Metadata</h3>
   857            <MetadataEditor
   858              @kv={{this.newMetaData}}
   859              @onEdit={{this.validateMetadata}}
   860            >
   861              <button
   862                data-test-new-metadata-button
   863                disabled={{or (not this.newMetaData.key) (not this.newMetaData.value)}}
   864                type="submit"
   865                class="button is-primary"
   866                {{on "click" (queue
   867                  (action this.addDynamicMetaData this.newMetaData)
   868                  this.resetNewMetaData
   869                  (action (mut this.editingMetadata) false)
   870                )}}
   871              >
   872                Add {{this.newMetaData.key}} to node metadata
   873              </button>
   874  
   875              <button
   876                type="button"
   877                class="button is-secondary"
   878                {{on "click" (queue
   879                  this.resetNewMetaData
   880                  (action (mut this.editingMetadata) false)
   881                )}}
   882              >
   883                Cancel
   884              </button>
   885  
   886            </MetadataEditor>
   887          </div>
   888        {{else}}
   889          <div class="add-dynamic-metadata">
   890            <button
   891              type="button"
   892              class="button is-primary"
   893              {{on "click" (action (mut this.editingMetadata) true)}}
   894              {{keyboard-shortcut
   895                label="Add Dynamic Node Metadata"
   896                pattern=(array "m" "e" "t" "a")
   897                action=(action (mut this.editingMetadata) true)
   898              }}
   899            >
   900              Add new Dynamic Metadata
   901            </button>
   902          </div>
   903        {{/if}}
   904      {{/if}}
   905    </div>
   906  </section>