This is an automated email from the ASF dual-hosted git repository.

ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new b514a3bc45 TPv2 bring Servers Detail/Table to parity with TPv1 (#7497)
b514a3bc45 is described below

commit b514a3bc45f7bd92f716a085a8fd502bab903daa
Author: Steve Hamrick <[email protected]>
AuthorDate: Mon May 22 15:30:10 2023 -0600

    TPv2 bring Servers Detail/Table to parity with TPv1 (#7497)
    
    * Servers detail parity
    
    * Cleanup
    
    * Code review fixes
    
    * Missed a comment
    
    * console.log
    
    * Wrong branch
    
    * Revert ISO changes, fix other comments
    
    * Update Types
    
    * Fix merge
    
    * Lint fix
    
    * Change selector to not rely on mat-card elements
    
    * Update selector
    
    * Missed a newline
    
    * Address comments
    
    * More logical width
    
    * Add test coverage
    
    * Regenerate package-lock
---
 CHANGELOG.md                                       |   1 -
 .../traffic-portal/build/package-lock.json         |   2 +-
 experimental/traffic-portal/build/package.json     |   2 +-
 .../traffic-portal/build/traffic_portal_v2.spec    |   2 +-
 .../traffic-portal/nightwatch/dataClient.ts        | 374 +++++++++++++++++++++
 .../traffic-portal/nightwatch/globals/globals.ts   | 359 +++-----------------
 .../nightwatch/globals/tables/index.ts             |  98 +++++-
 .../traffic-portal/nightwatch/nightwatch.conf.js   |   1 +
 .../page_objects/cacheGroups/asnDetail.ts          |  21 +-
 .../page_objects/cacheGroups/cacheGroupDetails.ts  |  32 +-
 .../page_objects/cacheGroups/coordinateDetail.ts   |  24 +-
 .../page_objects/cacheGroups/divisionDetail.ts     |  16 +-
 .../page_objects/cacheGroups/regionDetail.ts       |  21 +-
 .../nightwatch/page_objects/cdns/cdnDetail.ts      |  24 +-
 .../deliveryServices/deliveryServiceDetail.ts      |  36 +-
 .../deliveryServiceInvalidationJobs.ts             |   4 +-
 .../nightwatch/page_objects/login.ts               |  20 +-
 .../page_objects/servers/physLocDetail.ts          |  56 +--
 .../page_objects/servers/serversDetail.ts          |  68 ++++
 .../servers/{servers.ts => serversTable.ts}        |  27 +-
 .../page_objects/statuses/statusDetail.ts          |  20 +-
 .../nightwatch/page_objects/types/typeDetail.ts    |  24 +-
 .../nightwatch/page_objects/users/tenantDetail.ts  |  16 +-
 .../tests/servers/servers.detail.spec.ts           | 101 ++++++
 .../{servers.spec.ts => servers.table.spec.ts}     |   8 +-
 .../nightwatch/tests/users/users.spec.ts           |   2 +-
 experimental/traffic-portal/package-lock.json      |  16 +-
 experimental/traffic-portal/package.json           |   4 +-
 .../traffic-portal/src/app/api/cdn.service.spec.ts |  40 +++
 .../traffic-portal/src/app/api/cdn.service.ts      |  26 +-
 .../src/app/api/server.service.spec.ts             |  47 ++-
 .../traffic-portal/src/app/api/server.service.ts   |  36 ++
 .../src/app/api/testing/cdn.service.ts             |  35 +-
 .../src/app/api/testing/server.service.ts          |  48 ++-
 .../traffic-portal/src/app/app.ui.module.ts        |   9 +-
 .../traffic-portal/src/app/core/core.module.ts     |   2 +-
 .../server-details/_server-details-theme.scss      |   5 +
 .../server-details/server-details.component.html   | 239 ++++++++-----
 .../server-details/server-details.component.scss   | 181 +++++-----
 .../server-details.component.spec.ts               |  34 +-
 .../server-details/server-details.component.ts     | 241 +++++++++----
 .../servers-table/servers-table.component.html     |  33 +-
 .../servers-table/servers-table.component.spec.ts  |   4 +-
 .../servers-table/servers-table.component.ts       |  84 ++++-
 .../update-status/update-status.component.html     |   6 +-
 .../update-status/update-status.component.spec.ts  |   6 +-
 .../update-status/update-status.component.ts       |   5 +-
 .../generic-table/generic-table.component.html     |  30 +-
 .../generic-table/generic-table.component.scss     |  10 +-
 .../generic-table/generic-table.component.ts       |  63 +++-
 .../navigation/tp-header/tp-header.component.scss  |   5 +-
 .../tp-sidebar/tp-sidebar.component.scss           |   8 +-
 experimental/traffic-portal/src/styles.scss        | 117 ++++++-
 .../styles/vars.scss}                              |  37 +-
 54 files changed, 1788 insertions(+), 942 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c63c242c7..bed1df5502 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,7 +55,6 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 - [#7511](https://github.com/apache/trafficcontrol/pull/7511) *Traffic Ops* 
Fixed the changelog registration message to include the username instead of 
duplicate email entry.
-- [#7505](https://github.com/apache/trafficcontrol/pull/7505) *Traffic Portal* 
Fix an issue where a Delivery Service with Geo Limit Countries Set was unable 
to be updated.
 - [#7465](https://github.com/apache/trafficcontrol/issues/7465) *Traffic Ops* 
Fixes server_capabilities v5 apis to respond with RFC3339 date/time Format
 - [#7441](https://github.com/apache/trafficcontrol/pull/7441) *Traffic Ops* 
Fixed the invalidation jobs endpoint to respect CDN locks.
 - [#7413](https://github.com/apache/trafficcontrol/issues/7413) *Traffic Ops* 
Fixes service_category apis to respond with RFC3339 date/time Format
diff --git a/experimental/traffic-portal/build/package-lock.json 
b/experimental/traffic-portal/build/package-lock.json
index 0ee55d1b57..c8607e0ad7 100644
--- a/experimental/traffic-portal/build/package-lock.json
+++ b/experimental/traffic-portal/build/package-lock.json
@@ -12,7 +12,7 @@
         "pm2": "^5.2.2"
       },
       "engines": {
-        "node": ">=16.20"
+        "node": ">=16.14"
       }
     },
     "node_modules/@opencensus/core": {
diff --git a/experimental/traffic-portal/build/package.json 
b/experimental/traffic-portal/build/package.json
index 539fd2f709..9cd93d38ac 100644
--- a/experimental/traffic-portal/build/package.json
+++ b/experimental/traffic-portal/build/package.json
@@ -16,7 +16,7 @@
                "url": "https://trafficcontrol.apache.org";
        },
        "engines": {
-               "node": ">=16.20"
+               "node": ">=16.14"
        },
        "private": true,
        "license": "Apache-2.0",
diff --git a/experimental/traffic-portal/build/traffic_portal_v2.spec 
b/experimental/traffic-portal/build/traffic_portal_v2.spec
index 939468d6fe..4484747f3c 100644
--- a/experimental/traffic-portal/build/traffic_portal_v2.spec
+++ b/experimental/traffic-portal/build/traffic_portal_v2.spec
@@ -25,7 +25,7 @@ License:  Apache License, Version 2.0
 URL:      https://github.com/apache/trafficcontrol/
 Source:   %{_sourcedir}/traffic-portal-%{traffic_control_version}.tgz
 AutoReqProv: no
-Requires: nodejs >= 2:16.20.0
+Requires: nodejs >= 2:16.14.0
 Requires(pre): /usr/sbin/useradd, /usr/bin/getent
 
 %define traffic_portal_home /opt/traffic-portal
diff --git a/experimental/traffic-portal/nightwatch/dataClient.ts 
b/experimental/traffic-portal/nightwatch/dataClient.ts
new file mode 100644
index 0000000000..f7af8b1729
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/dataClient.ts
@@ -0,0 +1,374 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as https from "https";
+
+import axios, { AxiosError, AxiosInstance } from "axios";
+import { CreatedData } from "nightwatch/globals/globals";
+import {
+       CDN,
+       GeoLimit,
+       GeoProvider,
+       LoginRequest,
+       ProfileType,
+       Protocol,
+       RequestASN,
+       RequestCacheGroup,
+       RequestCoordinate,
+       RequestDeliveryService,
+       RequestDivision,
+       RequestPhysicalLocation,
+       RequestProfile,
+       RequestRegion,
+       RequestRole,
+       RequestServer,
+       RequestServerCapability,
+       RequestStatus,
+       RequestSteeringTarget,
+       RequestTenant,
+       RequestType,
+       ResponseCacheGroup,
+       ResponseDeliveryService,
+       ResponseDivision,
+       ResponsePhysicalLocation,
+       ResponseProfile,
+       ResponseRegion,
+       ResponseStatus,
+       TypeFromResponse
+} from "trafficops-types";
+
+/**
+ * Generates a unique string used for tests, uses the current epoch time.
+ *
+ * @returns a unique string
+ */
+export function generateUniqueString(): string {
+       return new Date().getTime().toString();
+}
+
+/**
+ * Defines the class used to create test data for the E2E environment
+ */
+export class DataClient {
+       private readonly toURL: string;
+       private readonly apiVersion: string;
+       private readonly adminUser: string;
+       private readonly adminPass: string;
+       /** Tracks if the client has logged in */
+       public loggedIn = false;
+       /** Client used to talk to the TO API */
+       private readonly client: AxiosInstance;
+
+       public constructor(toURL: string, apiVersion: string, adminUser: 
string, adminPass: string) {
+               this.toURL = toURL;
+               this.apiVersion = apiVersion;
+               this.adminUser = adminUser;
+               this.adminPass = adminPass;
+
+               this.client = axios.create({
+                       httpsAgent: new https.Agent({
+                               rejectUnauthorized: false
+                       })
+               });
+       }
+
+       /**
+        * Creates data needed for the E2E tests
+        *
+        * @param id ID added to various fields to ensure that creation occurs 
regardless of environment
+        */
+       public async createData(id: string): Promise<CreatedData> {
+               const apiUrl = `${this.toURL}/api/${this.apiVersion}`;
+               if 
(Object.keys(this.client.defaults.headers.common).indexOf("Cookie") === -1) {
+                       this.loggedIn = false;
+                       let accessToken = "";
+                       const loginReq: LoginRequest = {
+                               p: this.adminPass,
+                               u: this.adminUser
+                       };
+                       try {
+                               const logResp = await 
this.client.post(`${apiUrl}/user/login`, JSON.stringify(loginReq));
+                               if (logResp.headers["set-cookie"]) {
+                                       for (const cookie of 
logResp.headers["set-cookie"]) {
+                                               if 
(cookie.indexOf("access_token") > -1) {
+                                                       accessToken = cookie;
+                                                       break;
+                                               }
+                                       }
+                               }
+                       } catch (e) {
+                               console.error((e as AxiosError).message);
+                               throw e;
+                       }
+                       if (accessToken === "") {
+                               const e = new Error("Access token is not set");
+                               console.error(e.message);
+                               throw e;
+                       }
+                       this.loggedIn = true;
+                       this.client.defaults.headers.common = {Cookie: 
accessToken};
+               }
+
+               const cdn: CDN = {
+                       dnssecEnabled: false, domainName: `tests${id}.com`, 
name: `testCDN${id}`
+               };
+
+               let resp = await this.client.get(`${apiUrl}/types`);
+               const types: Array<TypeFromResponse> = resp.data.response;
+               const httpType = types.find(typ => typ.name === "HTTP" && 
typ.useInTable === "deliveryservice");
+               if (httpType === undefined) {
+                       throw new Error("Unable to find `HTTP` type");
+               }
+               const steeringType = types.find(typ => typ.name === "STEERING" 
&& typ.useInTable === "deliveryservice");
+               if (steeringType === undefined) {
+                       throw new Error("Unable to find `STEERING` type");
+               }
+               const steeringWeightType = types.find(typ => typ.name === 
"STEERING_WEIGHT" && typ.useInTable === "steering_target");
+               if (steeringWeightType === undefined) {
+                       throw new Error("Unable to find `STEERING_WEIGHT` 
type");
+               }
+               const cgType = types.find(typ => typ.useInTable === 
"cachegroup");
+               if (!cgType) {
+                       throw new Error("Unable to find any Cache Group Types");
+               }
+               const edgeType = types.find(typ => typ.useInTable === "server" 
&& typ.name === "EDGE");
+               if (edgeType === undefined) {
+                       throw new Error("Unable to find `EDGE` type");
+               }
+
+               const data = {} as CreatedData;
+               let url = `${apiUrl}/cdns`;
+               try {
+                       resp = await this.client.post(url, JSON.stringify(cdn));
+                       const respCDN = resp.data.response;
+                       data.cdn = respCDN;
+
+                       const ds: RequestDeliveryService = {
+                               active: false,
+                               cacheurl: null,
+                               cdnId: respCDN.id,
+                               displayName: `test DS${id}`,
+                               dscp: 0,
+                               ecsEnabled: false,
+                               edgeHeaderRewrite: null,
+                               fqPacingRate: null,
+                               geoLimit: GeoLimit.NONE,
+                               geoProvider: GeoProvider.MAX_MIND,
+                               httpBypassFqdn: null,
+                               infoUrl: null,
+                               initialDispersion: 1,
+                               ipv6RoutingEnabled: false,
+                               logsEnabled: false,
+                               maxOriginConnections: 0,
+                               maxRequestHeaderBytes: 0,
+                               midHeaderRewrite: null,
+                               missLat: 0,
+                               missLong: 0,
+                               multiSiteOrigin: false,
+                               orgServerFqdn: "http://test.com";,
+                               profileId: 1,
+                               protocol: Protocol.HTTP,
+                               qstringIgnore: 0,
+                               rangeRequestHandling: 0,
+                               regionalGeoBlocking: false,
+                               remapText: null,
+                               routingName: "test",
+                               signed: false,
+                               tenantId: 1,
+                               typeId: httpType.id,
+                               xmlId: `testDS${id}`
+                       };
+                       url = `${apiUrl}/deliveryservices`;
+                       resp = await this.client.post(url, JSON.stringify(ds));
+                       let respDS: ResponseDeliveryService = 
resp.data.response[0];
+                       data.ds = respDS;
+
+                       ds.displayName = `test DS2${id}`;
+                       ds.xmlId = `testDS2${id}`;
+                       resp = await this.client.post(url, JSON.stringify(ds));
+                       respDS = resp.data.response[0];
+                       data.ds2 = respDS;
+
+                       ds.displayName = `test steering DS${id}`;
+                       ds.xmlId = `testSDS${id}`;
+                       ds.typeId = steeringType.id;
+                       resp = await this.client.post(url, JSON.stringify(ds));
+                       respDS = resp.data.response[0];
+                       data.steeringDS = respDS;
+
+                       const target: RequestSteeringTarget = {
+                               targetId: data.ds.id,
+                               typeId: steeringWeightType.id,
+                               value: 1
+                       };
+                       url = 
`${apiUrl}/steering/${data.steeringDS.id}/targets`;
+                       await this.client.post(url, JSON.stringify(target));
+                       target.targetId = data.ds2.id;
+                       await this.client.post(url, JSON.stringify(target));
+
+                       const tenant: RequestTenant = {
+                               active: true,
+                               name: `testT${id}`,
+                               parentId: 1
+                       };
+                       url = `${apiUrl}/tenants`;
+                       resp = await this.client.post(url, 
JSON.stringify(tenant));
+                       data.tenant = resp.data.response;
+
+                       const division: RequestDivision = {
+                               name: `testD${id}`
+                       };
+                       url = `${apiUrl}/divisions`;
+                       resp = await this.client.post(url, 
JSON.stringify(division));
+                       const respDivision: ResponseDivision = 
resp.data.response;
+                       data.division = respDivision;
+
+                       const region: RequestRegion = {
+                               division: respDivision.id,
+                               name: `testR${id}`
+                       };
+                       url = `${apiUrl}/regions`;
+                       resp = await this.client.post(url, 
JSON.stringify(region));
+                       const respRegion: ResponseRegion = resp.data.response;
+                       data.region = respRegion;
+
+                       const cacheGroup: RequestCacheGroup = {
+                               name: `test${id}`,
+                               shortName: `test${id}`,
+                               typeId: cgType.id
+                       };
+                       url = `${apiUrl}/cachegroups`;
+                       resp = await this.client.post(url, 
JSON.stringify(cacheGroup));
+                       const responseCG: ResponseCacheGroup = 
resp.data.response;
+                       data.cacheGroup = responseCG;
+
+                       const asn: RequestASN = {
+                               asn: +id,
+                               cachegroupId: responseCG.id
+                       };
+                       url = `${apiUrl}/asns`;
+                       resp = await this.client.post(url, JSON.stringify(asn));
+                       data.asn = resp.data.response;
+
+                       const physLoc: RequestPhysicalLocation = {
+                               address: "street",
+                               city: "city",
+                               comments: "someone set us up the bomb",
+                               email: "[email protected]",
+                               name: `phys${id}`,
+                               phone: "111-867-5309",
+                               poc: "me",
+                               regionId: respRegion.id,
+                               shortName: `short${id}`,
+                               state: "CA",
+                               zip: "80000"
+                       };
+                       url = `${apiUrl}/phys_locations`;
+                       resp = await this.client.post(url, 
JSON.stringify(physLoc));
+                       const respPhysLoc: ResponsePhysicalLocation = 
resp.data.response;
+                       respPhysLoc.region = respRegion.name;
+                       data.physLoc = respPhysLoc;
+
+                       const coordinate: RequestCoordinate = {
+                               latitude: 0,
+                               longitude: 0,
+                               name: `coord${id}`
+                       };
+                       url = `${apiUrl}/coordinates`;
+                       resp = await this.client.post(url, 
JSON.stringify(coordinate));
+                       data.coordinate = resp.data.response;
+
+                       const type: RequestType = {
+                               description: "blah",
+                               name: `type${id}`,
+                               useInTable: "server"
+                       };
+                       url = `${apiUrl}/types`;
+                       resp = await this.client.post(url, 
JSON.stringify(type));
+
+                       data.type = resp.data.response;
+                       const status: RequestStatus = {
+                               description: "blah",
+                               name: `status${id}`,
+                       };
+                       url = `${apiUrl}/statuses`;
+                       resp = await this.client.post(url, 
JSON.stringify(status));
+                       const respStatus: ResponseStatus = resp.data.response;
+                       data.statuses = respStatus;
+
+                       const profile: RequestProfile = {
+                               cdn: respCDN.id,
+                               description: "blah",
+                               name: `profile${id}`,
+                               routingDisabled: false,
+                               type: ProfileType.ATS_PROFILE,
+                       };
+                       url = `${apiUrl}/profiles`;
+                       resp = await this.client.post(url, 
JSON.stringify(profile));
+                       const respProfile: ResponseProfile = resp.data.response;
+                       data.profile = respProfile;
+
+                       const server: RequestServer = {
+                               cachegroupId: responseCG.id,
+                               cdnId: respCDN.id,
+                               domainName: "domain.com",
+                               hostName: id,
+                               interfaces: [{
+                                       ipAddresses: [{
+                                               address: "192.160.1.0",
+                                               gateway: null,
+                                               serviceAddress: true
+                                       }],
+                                       maxBandwidth: 0,
+                                       monitor: true,
+                                       mtu: 1500,
+                                       name: "eth0"
+                               }],
+                               physLocationId: respPhysLoc.id,
+                               profileNames: [respProfile.name],
+                               statusId: respStatus.id,
+                               typeId: edgeType.id
+
+                       };
+                       url = `${apiUrl}/servers`;
+                       resp = await this.client.post(url, 
JSON.stringify(server));
+                       data.edgeServer = resp.data.response;
+
+                       const capability: RequestServerCapability = {
+                               name: `test${id}`
+                       };
+                       url = `${apiUrl}/server_capabilities`;
+                       resp = await this.client.post(url, 
JSON.stringify(capability));
+                       data.capability = resp.data.response;
+
+                       const role: RequestRole = {
+                               description: "Has access to everything - cannot 
be modified or deleted",
+                               name: `admin${id}`,
+                               permissions: [
+                                       "ALL"
+                               ]
+                       };
+                       url = `${apiUrl}/roles`;
+                       resp = await this.client.post(url, 
JSON.stringify(role));
+                       data.role = resp.data.response;
+               } catch (e) {
+                       const ae = e as AxiosError;
+                       ae.message = `Request (${ae.config.method}) failed to 
${url}`;
+                       ae.message += ae.response ? ` with response code 
${ae.response.status}` : " with no response";
+                       throw ae;
+               }
+
+               return data;
+       }
+}
diff --git a/experimental/traffic-portal/nightwatch/globals/globals.ts 
b/experimental/traffic-portal/nightwatch/globals/globals.ts
index 3ca3773ea6..b365baa2f6 100644
--- a/experimental/traffic-portal/nightwatch/globals/globals.ts
+++ b/experimental/traffic-portal/nightwatch/globals/globals.ts
@@ -12,9 +12,7 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-import * as https from "https";
-
-import axios, { AxiosError } from "axios";
+import { AxiosError } from "axios";
 import { NightwatchBrowser } from "nightwatch";
 import type { AsnDetailPageObject } from 
"nightwatch/page_objects/cacheGroups/asnDetail";
 import type { AsnsPageObject } from 
"nightwatch/page_objects/cacheGroups/asnsTable";
@@ -36,7 +34,8 @@ import type { ProfileDetailPageObject } from 
"nightwatch/page_objects/profiles/p
 import type { ProfilePageObject } from 
"nightwatch/page_objects/profiles/profilesTable";
 import type { PhysLocDetailPageObject } from 
"nightwatch/page_objects/servers/physLocDetail";
 import type { PhysLocTablePageObject } from 
"nightwatch/page_objects/servers/physLocTable";
-import type { ServersPageObject } from 
"nightwatch/page_objects/servers/servers";
+import type { ServersDetailPageObject } from 
"nightwatch/page_objects/servers/serversDetail";
+import type { ServersTablePageObject } from 
"nightwatch/page_objects/servers/serversTable";
 import type { StatusDetailPageObject } from 
"nightwatch/page_objects/statuses/statusDetail";
 import type { StatusesTablePageObject } from 
"nightwatch/page_objects/statuses/statusesTable";
 import type { ChangeLogsPageObject } from 
"nightwatch/page_objects/users/changeLogs";
@@ -45,44 +44,23 @@ import type { TenantDetailPageObject } from 
"nightwatch/page_objects/users/tenan
 import type { TenantsPageObject } from "nightwatch/page_objects/users/tenants";
 import type { UsersPageObject } from "nightwatch/page_objects/users/users";
 import {
-       GeoLimit,
-       GeoProvider,
-       ProfileType,
-       Protocol,
-
-       type CDN,
-       type LoginRequest,
-       type RequestASN,
-       type RequestCacheGroup,
-       type RequestCoordinate,
-       type RequestDeliveryService,
-       type RequestDivision,
-       type RequestPhysicalLocation,
-       type RequestProfile,
-       type RequestRegion,
-       type RequestRole,
-       type RequestServerCapability,
-       type RequestStatus,
-       type RequestSteeringTarget,
-       type RequestTenant,
-       type RequestType,
-       type ResponseASN,
-       type ResponseCacheGroup,
-       type ResponseCDN,
-       type ResponseCoordinate,
-       type ResponseDeliveryService,
-       type ResponseDivision,
-       type ResponsePhysicalLocation,
-       type ResponseProfile,
-       type ResponseRegion,
-       type ResponseRole,
-       type ResponseServerCapability,
-       type ResponseStatus,
-       type ResponseTenant,
-       type TypeFromResponse,
+       ResponseCDN,
+       ResponseDeliveryService,
+       ResponseTenant,
+       TypeFromResponse,
+       ResponseASN,
+       ResponseDivision,
+       ResponseRegion,
+       ResponseCacheGroup,
+       ResponsePhysicalLocation,
+       ResponseCoordinate,
+       ResponseStatus,
+       ResponseProfile,
+       ResponseServer, ResponseServerCapability, ResponseRole,
 } from "trafficops-types";
 
 import * as config from "../config.json";
+import { DataClient, generateUniqueString } from "../dataClient";
 import type { CapabilitiesPageObject } from 
"../page_objects/servers/capabilities/capabilitiesTable";
 import type { CapabilityDetailsPageObject } from 
"../page_objects/servers/capabilities/capabilityDetails";
 import type { TypeDetailPageObject } from "../page_objects/types/typeDetail";
@@ -126,7 +104,8 @@ declare module "nightwatch" {
                        };
                        physLocDetail: () => PhysLocDetailPageObject;
                        physLocTable: () => PhysLocTablePageObject;
-                       servers: () => ServersPageObject;
+                       serversTable: () => ServersTablePageObject;
+                       serversDetail: () => ServersDetailPageObject;
                };
                statuses: {
                        statusesTable: () => StatusesTablePageObject;
@@ -170,21 +149,40 @@ export interface CreatedData {
        division: ResponseDivision;
        ds: ResponseDeliveryService;
        ds2: ResponseDeliveryService;
-       profile: ResponseProfile;
+       edgeServer: ResponseServer;
        physLoc: ResponsePhysicalLocation;
        region: ResponseRegion;
        role: ResponseRole;
-       statuses: ResponseStatus;
        steeringDS: ResponseDeliveryService;
        tenant: ResponseTenant;
        type: TypeFromResponse;
+       statuses: ResponseStatus;
+       profile: ResponseProfile;
 }
 
-const testData = {};
+let testData = {};
+let client: DataClient;
+let dataCreateFailed = false;
 
 const globals = {
        adminPass: config.adminPass,
        adminUser: config.adminUser,
+       after: async (done: () => void): Promise<void> => {
+               if (dataCreateFailed){
+                       return done();
+               } else if(client.loggedIn) {
+                       try {
+                               await client.createData(generateUniqueString());
+                       } catch(e) {
+                               console.error("Idempotency test failed, err:", 
e);
+                               throw e;
+                       }
+                       console.log("Data creation is idempotent");
+               } else {
+                       console.log("Client not logged in, skipping idempotency 
test");
+               }
+               done();
+       },
        afterEach: (browser: NightwatchBrowser, done: () => void): void => {
                browser.end(() => {
                        done();
@@ -192,277 +190,12 @@ const globals = {
        },
        apiVersion: "4.0",
        before: async (done: () => void): Promise<void> => {
-               const apiUrl = 
`${globals.trafficOpsURL}/api/${globals.apiVersion}`;
-               const client = axios.create({
-                       httpsAgent: new https.Agent({
-                               rejectUnauthorized: false
-                       })
-               });
-               let accessToken = "";
-               const loginReq: LoginRequest = {
-                       p: globals.adminPass,
-                       u: globals.adminUser
-               };
+               client = new DataClient(globals.trafficOpsURL, 
globals.apiVersion, globals.adminUser, globals.adminPass);
                try {
-                       const logResp = await 
client.post(`${apiUrl}/user/login`, JSON.stringify(loginReq));
-                       if(logResp.headers["set-cookie"]) {
-                               for (const cookie of 
logResp.headers["set-cookie"]) {
-                                       if(cookie.indexOf("access_token") > -1) 
{
-                                               accessToken = cookie;
-                                               break;
-                                       }
-                               }
-                       }
-               } catch (e) {
-                       console.error((e as AxiosError).message);
-                       throw e;
-               }
-               if(accessToken === "") {
-                       const e = new Error("Access token is not set");
-                       console.error(e.message);
-                       throw e;
-               }
-               client.defaults.headers.common = { Cookie: accessToken };
-
-               const cdn: CDN = {
-                       dnssecEnabled: false, domainName: 
`tests${globals.uniqueString}.com`, name: `testCDN${globals.uniqueString}`
-               };
-               let respCDN: ResponseCDN;
-
-               let resp = await client.get(`${apiUrl}/types`);
-               const types: Array<TypeFromResponse> = resp.data.response;
-               const httpType = types.find(typ => typ.name === "HTTP" && 
typ.useInTable === "deliveryservice");
-               if(httpType === undefined) {
-                       throw new Error("Unable to find `HTTP` type");
-               }
-               const steeringType = types.find(typ => typ.name === "STEERING" 
&& typ.useInTable === "deliveryservice");
-               if(steeringType === undefined) {
-                       throw new Error("Unable to find `STEERING` type");
-               }
-               const steeringWeightType = types.find(typ => typ.name === 
"STEERING_WEIGHT" && typ.useInTable === "steering_target");
-               if(steeringWeightType === undefined) {
-                       throw new Error("Unable to find `STEERING_WEIGHT` 
type");
-               }
-               const cgType = types.find(typ => typ.useInTable === 
"cachegroup");
-               if (!cgType) {
-                       throw new Error("Unable to find any Cache Group Types");
-               }
-
-               let url = `${apiUrl}/cdns`;
-               try {
-                       const data = testData as CreatedData;
-                       resp = await client.post(url, JSON.stringify(cdn));
-                       respCDN = resp.data.response;
-                       console.log(`Successfully created CDN ${respCDN.name}`);
-                       data.cdn = respCDN;
-
-                       const ds: RequestDeliveryService = {
-                               active: false,
-                               cacheurl: null,
-                               cdnId: respCDN.id,
-                               displayName: `test DS${globals.uniqueString}`,
-                               dscp: 0,
-                               ecsEnabled: false,
-                               edgeHeaderRewrite: null,
-                               fqPacingRate: null,
-                               geoLimit: GeoLimit.NONE,
-                               geoProvider: GeoProvider.MAX_MIND,
-                               httpBypassFqdn: null,
-                               infoUrl: null,
-                               initialDispersion: 1,
-                               ipv6RoutingEnabled: false,
-                               logsEnabled: false,
-                               maxOriginConnections: 0,
-                               maxRequestHeaderBytes: 0,
-                               midHeaderRewrite: null,
-                               missLat: 0,
-                               missLong: 0,
-                               multiSiteOrigin: false,
-                               orgServerFqdn: "http://test.com";,
-                               profileId: 1,
-                               protocol: Protocol.HTTP,
-                               qstringIgnore: 0,
-                               rangeRequestHandling: 0,
-                               regionalGeoBlocking: false,
-                               remapText: null,
-                               routingName: "test",
-                               signed: false,
-                               tenantId: 1,
-                               typeId: httpType.id,
-                               xmlId: `testDS${globals.uniqueString}`
-                       };
-                       url = `${apiUrl}/deliveryservices`;
-                       resp = await client.post(url, JSON.stringify(ds));
-                       let respDS: ResponseDeliveryService = 
resp.data.response[0];
-                       console.log(`Successfully created DS 
'${respDS.displayName}'`);
-                       data.ds = respDS;
-
-                       ds.displayName = `test DS2${globals.uniqueString}`;
-                       ds.xmlId = `testDS2${globals.uniqueString}`;
-                       resp = await client.post(url, JSON.stringify(ds));
-                       respDS = resp.data.response[0];
-                       console.log(`Successfully created DS 
'${respDS.displayName}'`);
-                       data.ds2 = respDS;
-
-                       ds.displayName = `test steering 
DS${globals.uniqueString}`;
-                       ds.xmlId = `testSDS${globals.uniqueString}`;
-                       ds.typeId = steeringType.id;
-                       resp = await client.post(url, JSON.stringify(ds));
-                       respDS = resp.data.response[0];
-                       console.log(`Successfully created DS 
'${respDS.displayName}'`);
-                       data.steeringDS = respDS;
-
-                       const target: RequestSteeringTarget = {
-                               targetId: data.ds.id,
-                               typeId: steeringWeightType.id,
-                               value: 1
-                       };
-                       url = 
`${apiUrl}/steering/${data.steeringDS.id}/targets`;
-                       await client.post(url, JSON.stringify(target));
-                       target.targetId = data.ds2.id;
-                       await client.post(url, JSON.stringify(target));
-                       console.log(`Created steering targets for 
${data.steeringDS.displayName}`);
-
-                       const tenant: RequestTenant = {
-                               active: true,
-                               name: `testT${globals.uniqueString}`,
-                               parentId: 1
-                       };
-                       url = `${apiUrl}/tenants`;
-                       resp = await client.post(url, JSON.stringify(tenant));
-                       const respTenant: ResponseTenant = resp.data.response;
-                       console.log(`Successfully created Tenant 
${respTenant.name}`);
-                       data.tenant = respTenant;
-
-                       const division: RequestDivision = {
-                               name: `testD${globals.uniqueString}`
-                       };
-                       url = `${apiUrl}/divisions`;
-                       resp = await client.post(url, JSON.stringify(division));
-                       const respDivision: ResponseDivision = 
resp.data.response;
-                       console.log(`Successfully created Division 
${respDivision.name}`);
-                       data.division = respDivision;
-
-                       const region: RequestRegion = {
-                               division: respDivision.id,
-                               name: `testR${globals.uniqueString}`
-                       };
-                       url = `${apiUrl}/regions`;
-                       resp = await client.post(url, JSON.stringify(region));
-                       const respRegion: ResponseRegion = resp.data.response;
-                       console.log(`Successfully created Region 
${respRegion.name}`);
-                       data.region = respRegion;
-
-                       const cacheGroup: RequestCacheGroup = {
-                               name: `test${globals.uniqueString}`,
-                               shortName: `test${globals.uniqueString}`,
-                               typeId: cgType.id
-                       };
-                       url = `${apiUrl}/cachegroups`;
-                       resp = await client.post(url, 
JSON.stringify(cacheGroup));
-                       const responseCG: ResponseCacheGroup = 
resp.data.response;
-                       console.log("Successfully created Cache Group:", 
responseCG.name);
-                       data.cacheGroup = responseCG;
-
-                       const asn: RequestASN = {
-                               asn: +globals.uniqueString,
-                               cachegroupId: responseCG.id
-                       };
-                       url = `${apiUrl}/asns`;
-                       resp = await client.post(url, JSON.stringify(asn));
-                       const respAsn: ResponseASN = resp.data.response;
-                       console.log(`Successfully created ASN ${respAsn.asn}`);
-                       data.asn = respAsn;
-
-                       const physLoc: RequestPhysicalLocation = {
-                               address: "street",
-                               city: "city",
-                               comments: "someone set us up the bomb",
-                               email: "[email protected]",
-                               name: `phys${globals.uniqueString}`,
-                               phone: "111-867-5309",
-                               poc: "me",
-                               regionId: respRegion.id,
-                               shortName: `short${globals.uniqueString}`,
-                               state: "CA",
-                               zip: "80000"
-                       };
-                       url = `${apiUrl}/phys_locations`;
-                       resp = await client.post(url, JSON.stringify(physLoc));
-                       const respPhysLoc: ResponsePhysicalLocation = 
resp.data.response;
-                       respPhysLoc.region = respRegion.name;
-                       console.log(`Successfully created Phys Loc 
${respPhysLoc.name}`);
-                       data.physLoc = respPhysLoc;
-
-                       const coordinate: RequestCoordinate = {
-                               latitude: 0,
-                               longitude: 0,
-                               name: `coord${globals.uniqueString}`
-                       };
-                       url = `${apiUrl}/coordinates`;
-                       resp = await client.post(url, 
JSON.stringify(coordinate));
-                       const respCoordinate: ResponseCoordinate = 
resp.data.response;
-                       console.log(`Successfully created Coordinate 
${respCoordinate.name}`);
-                       data.coordinate = respCoordinate;
-
-                       const type: RequestType = {
-                               description: "blah",
-                               name: `type${globals.uniqueString}`,
-                               useInTable: "server"
-                       };
-                       url = `${apiUrl}/types`;
-                       resp = await client.post(url, JSON.stringify(type));
-                       const respType: TypeFromResponse = resp.data.response;
-                       console.log(`Successfully created Type 
${respType.name}`);
-                       data.type = respType;
-
-                       const status: RequestStatus = {
-                               description: "blah",
-                               name: `status${globals.uniqueString}`,
-                       };
-                       url = `${apiUrl}/statuses`;
-                       resp = await client.post(url, JSON.stringify(status));
-                       const respStatus: ResponseStatus = resp.data.response;
-                       console.log(`Successfully created Status 
${respStatus.name}`);
-                       data.statuses = respStatus;
-
-                       const profile: RequestProfile = {
-                               cdn: 1,
-                               description: "blah",
-                               name: `profile${globals.uniqueString}`,
-                               routingDisabled: false,
-                               type: ProfileType.ATS_PROFILE,
-                       };
-                       url = `${apiUrl}/profiles`;
-                       resp = await client.post(url, JSON.stringify(profile));
-                       const respProfile: ResponseProfile = resp.data.response;
-                       console.log(`Successfully created Profile 
${respProfile.name}`);
-                       data.profile = respProfile;
-
-                       const capability: RequestServerCapability = {
-                               name: `test${globals.uniqueString}`
-                       };
-                       url = `${apiUrl}/server_capabilities`;
-                       resp = await client.post(url, 
JSON.stringify(capability));
-                       const respCap: ResponseServerCapability = 
resp.data.response;
-                       console.log("Successfully created Capability:", 
respCap);
-                       data.capability = respCap;
-
-                       const role: RequestRole = {
-                               description: "Has access to everything - cannot 
be modified or deleted",
-                               name: `admin${globals.uniqueString}`,
-                               permissions: [
-                                       "ALL"
-                               ]
-                       };
-                       url = `${apiUrl}/roles`;
-                       resp = await client.post(url, JSON.stringify(role));
-                       const respRole: ResponseRole = resp.data.response;
-                       console.log(`Successfully created Roles 
${respRole.name}`);
-                       data.role = respRole;
-
+                       testData = await 
client.createData(globals.uniqueString);
                } catch(e) {
-                       console.error("Request for", url, "failed:", (e as 
AxiosError).message);
+                       dataCreateFailed = true;
+                       console.error("Request for", globals.trafficOpsURL, 
"failed:", (e as AxiosError).message);
                        throw e;
                }
                done();
@@ -480,7 +213,7 @@ const globals = {
        retryAssertionTimeout: config.retryAssertionTimeoutMS,
        testData,
        trafficOpsURL: config.to_url,
-       uniqueString: new Date().getTime().toString(),
+       uniqueString: generateUniqueString(),
        waitForConditionTimeout:config.waitForConditionTimeoutMS
 };
 
diff --git a/experimental/traffic-portal/nightwatch/globals/tables/index.ts 
b/experimental/traffic-portal/nightwatch/globals/tables/index.ts
index e9b5ce4b67..b9c5d1b0d3 100644
--- a/experimental/traffic-portal/nightwatch/globals/tables/index.ts
+++ b/experimental/traffic-portal/nightwatch/globals/tables/index.ts
@@ -12,29 +12,55 @@
 * limitations under the License.
 */
 
-import type { Awaitable, EnhancedElementInstance, EnhancedPageObject, 
EnhancedSectionInstance } from "nightwatch";
+import type {
+       Awaitable,
+       EnhancedElementInstance,
+       EnhancedPageObject,
+       EnhancedSectionInstance,
+       WebDriverProtocolUserActions
+} from "nightwatch";
 
 /**
  * TableSectionCommands is the base type for page object sections representing
  * pages containing AG-Grid generic tables.
  */
-export interface TableSectionCommands extends EnhancedSectionInstance, 
EnhancedElementInstance<EnhancedPageObject> {
+export interface TableSectionCommands extends EnhancedSectionInstance,
+       EnhancedElementInstance<EnhancedPageObject>, 
WebDriverProtocolUserActions {
+       doubleClickRow<T extends this>(row: number): Promise<T>;
+
+       filterTableByColumn<T extends this>(column: string, search: string): 
Promise<T>;
+
+       gotoRowByColumn<T extends this>(column: string, search: string): 
Promise<T>;
+
        getColumnState(column: string): Promise<boolean>;
+
        searchText<T extends this>(text: string): T;
-       toggleColumn<T extends this>(column: string): T;
+
+       toggleColumn(column: string): Promise<this>;
 }
 
 /**
  * A CSS selector for an AG-Grid generic table's column visibility dropdown
  * menu.
  */
-export const columnMenuSelector = "div.toggle-columns > 
button.mat-mdc-menu-trigger";
+export const columnMenuBtnSelector = "div.toggle-columns > 
button.mat-mdc-menu-trigger";
+
+/**
+ * A CSS selector for an AG-Grid generic table's column visibility dropdown
+ * menu.
+ */
+export const columnMenuCloseSelector = ".cdk-overlay-backdrop";
 
 /**
  * A CSS selector for an AG-Grid generic table's "Fuzzy Search" input text box.
  */
 export const searchboxSelector = "input[name='fuzzControl']";
 
+/**
+ * CSS selector for the AG-Grid row(s).
+ */
+export const tableRowsSelector = ".ag-center-cols-clipper .ag-row";
+
 /**
  * Gets the state of an AG-Grid column by checking whether it's checked
  * in the column visibility menu (doesn't actually verify that this means the
@@ -48,9 +74,55 @@ export const searchboxSelector = "input[name='fuzzControl']";
  */
 export async function getColumnState(this: TableSectionCommands, column: 
string): Promise<boolean> {
        const selector = `input[type='checkbox'][name='${column}']`;
-       return this.click(columnMenuSelector).parent
-               .getLocationInView(selector)
-               .isSelected(selector);
+       await this.click(columnMenuBtnSelector);
+       const selected = await browser.isSelected(selector);
+       return Promise.resolve(selected);
+}
+
+/**
+ * Filters a table by a column
+ *
+ * @param this Special parameter that tells the compiler what `this` is in a
+ * valid context for this function.
+ * @param column Which column to filter
+ * @param text Text to filter by
+ */
+export async function filterTableByColumn<T extends TableSectionCommands>(
+       this: TableSectionCommands,
+       column: string,
+       text: string): Promise<T> {
+       if (!await this.getColumnState(column)) {
+               await this.toggleColumn(column);
+       }
+       this.searchText(text);
+       return Promise.resolve(this) as Promise<T>;
+}
+
+/**
+ * Double-clicks the nth row on a table
+ *
+ * @param this Special parameter that tells the compiler what `this` is in a
+ * valid context for this function.
+ * @param rowNumber Which row to click
+ * @returns The calling command section for call-chaining the way Nightwatch
+ * likes to do.
+ */
+export function doubleClickRow<T extends TableSectionCommands>(this: 
TableSectionCommands, rowNumber: number): Awaitable<T, null> {
+       return this.doubleClick("css selector", 
`${tableRowsSelector}:nth-of-type(${rowNumber})`) as Awaitable<T, null>;
+}
+
+/**
+ * Filters a table by a column, then double-clicks the first row resulting 
from the filtering.
+ *
+ * @param this Special parameter that tells the compiler what `this` is in a
+ * valid context for this function.
+ * @param column Which column to filter
+ * @param text Text to filter by
+ */
+export async function gotoRowByColumn<T extends TableSectionCommands>(
+       this: TableSectionCommands, column: string, text: string): Promise<T> {
+       await this.filterTableByColumn(column, text);
+       return this.doubleClickRow(1);
 }
 
 /**
@@ -75,8 +147,13 @@ export function searchText<T extends 
TableSectionCommands>(this: T, text: string
  * @returns The calling command section for call-chaining the way Nightwatch
  * likes to do.
  */
-export function toggleColumn<T extends TableSectionCommands>(this: T, column: 
string): Awaitable<T, null> {
-       return 
this.click(columnMenuSelector).click(`mat-input[name='${column}']`).click(columnMenuSelector)
 as Awaitable<T, null>;
+export async function toggleColumn<T extends TableSectionCommands>(this: T, 
column: string): Promise<T> {
+       const selector = `input[type='checkbox'][name='${column}']`;
+       await browser.findElement(".mat-mdc-menu-panel")
+               .getLocationInView(selector)
+               .click(selector);
+       await browser.click(columnMenuCloseSelector);
+       return Promise.resolve(this);
 }
 
 /**
@@ -84,7 +161,10 @@ export function toggleColumn<T extends 
TableSectionCommands>(this: T, column: st
  * to most easily provide all the functionality of a table.
  */
 export const TABLE_COMMANDS = {
+       doubleClickRow,
+       filterTableByColumn,
        getColumnState,
+       gotoRowByColumn,
        searchText,
        toggleColumn
 };
diff --git a/experimental/traffic-portal/nightwatch/nightwatch.conf.js 
b/experimental/traffic-portal/nightwatch/nightwatch.conf.js
index 4f2f1ac590..0d91229e3e 100644
--- a/experimental/traffic-portal/nightwatch/nightwatch.conf.js
+++ b/experimental/traffic-portal/nightwatch/nightwatch.conf.js
@@ -95,6 +95,7 @@ module.exports = {
                                "goog:chromeOptions": {
                                        args: [
                                                "--ignore-certificate-errors",
+                                               "--window-size=1920,1080",
                                                "--allow-insecure-localhost"
                                        ]
                                }
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/asnDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/asnDetail.ts
index 5ed7516fa0..4548996aa0 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/asnDetail.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/asnDetail.ts
@@ -21,22 +21,11 @@ export type AsnDetailPageObject = EnhancedPageObject<{}, 
typeof asnDetailPageObj
 
 const asnDetailPageObject = {
        elements: {
-               asn: {
-                       selector: "input[name='asn']"
-               },
-               cachegroup: {
-                       selector: "mat-select[name='cachegroup']"
-               },
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-
-               saveBtn: {
-                       selector: "button[type='submit']"
-               }
+               asn: "input[name='asn']",
+               cachegroup: "mat-select[name='cachegroup']",
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               saveBtn: "button[type='submit']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/cacheGroupDetails.ts
 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/cacheGroupDetails.ts
index 973b66bded..96a9f29a87 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/cacheGroupDetails.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/cacheGroupDetails.ts
@@ -21,30 +21,14 @@ export type CacheGroupDetailPageObject = 
EnhancedPageObject<{}, typeof cacheGrou
 
 const cacheGroupDetailPageObject = {
        elements: {
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-               latitude: {
-                       selector: "input[name='latitude']"
-               },
-               longitude: {
-                       selector: "input[name='longitude']"
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-               parent: {
-                       selector: "mat-select[name='parentCacheGroup']"
-               },
-               saveBtn: {
-                       selector: "button[type='submit']"
-               },
-               secondaryParent: {
-                       selector: "mat-select[name='secondaryParentCacheGroup]"
-               },
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               latitude: "input[name='latitude']",
+               longitude: "input[name='longitude']",
+               name: "input[name='name']",
+               parent: "mat-select[name='parentCacheGroup']",
+               saveBtn: "button[type='submit']",
+               secondaryParent: "mat-select[name='secondaryParentCacheGroup]",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/coordinateDetail.ts
 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/coordinateDetail.ts
index a9823030f9..18a2db2e05 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/coordinateDetail.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/coordinateDetail.ts
@@ -21,24 +21,12 @@ export type CoordinateDetailPageObject = 
EnhancedPageObject<{}, typeof coordinat
 
 const coordinateDetailPageObject = {
        elements: {
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-               latitude: {
-                       selector: "input[name='latitude']"
-               },
-               longitude: {
-                       selector: "input[name='longitude']"
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-               saveBtn: {
-                       selector: "button[type='submit']"
-               }
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               latitude: "input[name='latitude']",
+               longitude: "input[name='longitude']",
+               name: "input[name='name']",
+               saveBtn: "button[type='submit']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/divisionDetail.ts
 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/divisionDetail.ts
index 71cd31f849..9bdbdeb1bd 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/divisionDetail.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/divisionDetail.ts
@@ -21,18 +21,10 @@ export type DivisionDetailPageObject = 
EnhancedPageObject<{}, typeof divisionDet
 
 const divisionDetailPageObject = {
        elements: {
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-               saveBtn: {
-                       selector: "button[type='submit']"
-               }
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               name: "input[name='name']",
+               saveBtn: "button[type='submit']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/regionDetail.ts
 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/regionDetail.ts
index 3f2d6b4de6..62d8cd5593 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/regionDetail.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/cacheGroups/regionDetail.ts
@@ -21,22 +21,11 @@ export type RegionDetailPageObject = EnhancedPageObject<{}, 
typeof regionDetailP
 
 const regionDetailPageObject = {
        elements: {
-               division: {
-                       selector: "mat-select[name='division']"
-               },
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-
-               saveBtn: {
-                       selector: "button[type='submit']"
-               }
+               division: "mat-select[name='division']",
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               name: "input[name='name']",
+               saveBtn: "button[type='submit']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/cdns/cdnDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/cdns/cdnDetail.ts
index cad2861acd..f33c90de0c 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/cdns/cdnDetail.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/cdns/cdnDetail.ts
@@ -21,24 +21,12 @@ export type CDNDetailPageObject = EnhancedPageObject<{}, 
typeof cdnDetailPageObj
 
 const cdnDetailPageObject = {
        elements: {
-               dnssecEnabled: {
-                       selector: "input[name='dnssecEnabled']"
-               },
-               domainName: {
-                       selector: "input[name='domainName']"
-               },
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-               saveBtn: {
-                       selector: "button[type='submit']"
-               },
+               dnssecEnabled: "input[name='dnssecEnabled']",
+               domainName: "input[name='domainName']",
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               name: "input[name='name']",
+               saveBtn: "button[type='submit']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceDetail.ts
 
b/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceDetail.ts
index 2c9639aee1..661846f3f7 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceDetail.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceDetail.ts
@@ -23,37 +23,19 @@ export type DeliveryServiceDetailPageObject = 
EnhancedPageObject<{}, typeof deli
 
 const deliveryServiceDetailPageObject = {
        elements: {
-               bandwidthChart: {
-                       selector: "canvas#bandwidthData"
-               },
-               invalidateJobs: {
-                       selector: "a#invalidate"
-               },
-               tpsChart: {
-                       selector: "canvas#tpsChartData"
-               },
+               bandwidthChart: "canvas#bandwidthData",
+               invalidateJobs: "a#invalidate",
+               tpsChart: "canvas#tpsChartData",
        },
        sections: {
                dateInputForm: {
                        elements: {
-                               fromDate: {
-                                       selector: "input[name='fromdate']"
-                               },
-                               fromTime: {
-                                       selector: "input[name='fromtime']"
-                               },
-                               refreshBtn: {
-                                       selector: 
"button[name='timespanRefresh']"
-                               },
-                               steeringIcon: {
-                                       selector: "div.actions > mat-icon"
-                               },
-                               toDate: {
-                                       selector: "input[name='todate']"
-                               },
-                               toTime: {
-                                       selector: "input[name='totime']"
-                               }
+                               fromDate: "input[name='fromdate']",
+                               fromTime: "input[name='fromtime']",
+                               refreshBtn: "button[name='timespanRefresh']",
+                               steeringIcon: "div.actions > mat-icon",
+                               toDate: "input[name='todate']",
+                               toTime: "input[name='totime']",
                        },
                        selector: "form[name='timespan']"
                }
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs.ts
 
b/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs.ts
index e9e3f80812..5ce3e4117b 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs.ts
@@ -23,9 +23,7 @@ export type DeliveryServiceInvalidPageObject = 
EnhancedPageObject<{}, typeof del
 
 const deliveryServiceInvalidPageObject = {
        elements: {
-               addButton: {
-                       selector: "button#new"
-               }
+               addButton: "button#new",
        }
 };
 
diff --git a/experimental/traffic-portal/nightwatch/page_objects/login.ts 
b/experimental/traffic-portal/nightwatch/page_objects/login.ts
index fdd68cb0d2..1a598ba0c4 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/login.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/login.ts
@@ -57,21 +57,11 @@ const loginPageObject = {
                                }
                        } as LoginFormSectionCommands,
                        elements: {
-                               clearBtn: {
-                                       selector: "button[name='clear']"
-                               },
-                               loginBtn: {
-                                       selector: "button[name='login']"
-                               },
-                               passwordTxt: {
-                                       selector: "input[name='p']"
-                               },
-                               resetBtn: {
-                                       selector: "button[name='reset']"
-                               },
-                               usernameTxt: {
-                                       selector: "input[name='u']"
-                               }
+                               clearBtn: "button[name='clear']",
+                               loginBtn: "button[name='login']",
+                               passwordTxt: "input[name='p']",
+                               resetBtn: "button[name='reset']",
+                               usernameTxt: "input[name='u']",
                        },
                        selector: "form[name='login']"
                }
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/servers/physLocDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/servers/physLocDetail.ts
index 0ffe4a8c7b..a1f3a0922e 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/servers/physLocDetail.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/servers/physLocDetail.ts
@@ -21,48 +21,20 @@ export type PhysLocDetailPageObject = 
EnhancedPageObject<{}, typeof physLocDetai
 
 const physLocDetailPageObject = {
        elements: {
-               address: {
-                       selector: "input[name='address']"
-               },
-               city: {
-                       selector: "input[name='city']"
-               },
-               comments: {
-                       selector: "textarea[name='comments']"
-               },
-               email: {
-                       selector: "input[name='email']"
-               },
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-               phone: {
-                       selector: "input[name='phone']"
-               },
-               poc: {
-                       selector: "input[name='poc']"
-               },
-               region: {
-                       selector: "mat-select[name='region']"
-               },
-               saveBtn: {
-                       selector: "button[type='submit']"
-               },
-               shortName: {
-                       selector: "input[name='shortName']"
-               },
-               state: {
-                       selector: "input[name='state']"
-               },
-               zip: {
-                       selector: "input[name='postalCode']"
-               }
+               address: "input[name='address']",
+               city: "input[name='city']",
+               comments: "textarea[name='comments']",
+               email: "input[name='email']",
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               name: "input[name='name']",
+               phone: "input[name='phone']",
+               poc: "input[name='poc']",
+               region: "mat-select[name='region']",
+               saveBtn: "button[type='submit']",
+               shortName: "input[name='shortName']",
+               state: "input[name='state']",
+               zip: "input[name='postalCode']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/servers/serversDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/servers/serversDetail.ts
new file mode 100644
index 0000000000..790b6c17c0
--- /dev/null
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/servers/serversDetail.ts
@@ -0,0 +1,68 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { EnhancedPageObject, EnhancedSectionInstance } from "nightwatch";
+
+/**
+ * Defines the Section Instance for the Servers Details.
+ */
+type ServersDetailSection = EnhancedSectionInstance<
+EnhancedSectionInstance<typeof 
serversDetailPageObject.sections.detailCard.commands,
+               typeof serversDetailPageObject.sections.detailCard.elements>>;
+
+/**
+ * Defines the PageObject for Servers Details.
+ */
+export type ServersDetailPageObject = EnhancedPageObject<{}, {}, { detailCard: 
ServersDetailSection}>;
+
+const serversDetailPageObject = {
+       sections: {
+               detailCard: {
+                       commands: {
+                       },
+                       elements: {
+                               cacheGroup: "mat-select[name='cachegroup']",
+                               cdn: "mat-select[name='cdn']",
+                               deleteBtn: "button[aria-label='Delete Server']",
+                               domainName: "input[name='domainname']",
+                               hostName: "input[name='hostname']",
+                               httpPort: "input[name='httpport']",
+                               httpsPort: "input[name='httpsport']",
+                               id: "input[name='serverId']",
+                               iloGateway: "input[name='iloGateway']",
+                               iloIP: "input[name='iloIP']",
+                               iloNetmask: "input[name='iloNetmask']",
+                               iloPassword: "input[name='iloPassword']",
+                               iloUsername: "input[name='iloUsername']",
+                               intfAddBtn: "button[aria-label='Add An 
Interface']",
+                               lastUpdated: "input[name='lastUpdated']",
+                               mgmtGateway: "input[name='mgmtIpGateway']",
+                               mgmtIP: "input[name='mgmtIP']",
+                               mgmtNetmask: "input[name='mgmtIpNetmask']",
+                               offlineReason: 
"mat-select[name='offlineReason']",
+                               physLoc: "mat-select[name='physLocation']",
+                               profileNames: "mat-select[name='profiles']",
+                               rack: "input[name='rack']",
+                               status: "mat-select[name='status']",
+                               statusDisabled: "input[name='status']",
+                               statusLastUpdated: 
"input[name='statusLastUpdated']",
+                               submitBtn: "button[aria-label='Submit Server']",
+                               type: "mat-select[name='type']"
+                       },
+                       selector: "mat-card.page-content"
+               }
+       }
+};
+
+export default serversDetailPageObject;
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/servers/servers.ts 
b/experimental/traffic-portal/nightwatch/page_objects/servers/serversTable.ts
similarity index 59%
rename from 
experimental/traffic-portal/nightwatch/page_objects/servers/servers.ts
rename to 
experimental/traffic-portal/nightwatch/page_objects/servers/serversTable.ts
index bb57414631..5e3f2e93d6 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/servers/servers.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/servers/serversTable.ts
@@ -16,24 +16,45 @@ import {
        EnhancedSectionInstance,
        NightwatchAPI
 } from "nightwatch";
+import { type ResponseServer } from "trafficops-types";
 
 import { TableSectionCommands, TABLE_COMMANDS } from "../../globals/tables";
 
 /**
  * Defines the commands for the servers table section.
  */
-type ServersTableSectionCommands = TableSectionCommands;
+interface ServersTableSectionCommands extends TableSectionCommands {
+       createNew(): Promise<void>;
+       openDetails(s: ResponseServer): Promise<void>;
+       open(): Promise<void>;
+}
 
 const serversPageObject = {
        api: {} as NightwatchAPI,
        sections: {
                serversTable: {
                        commands: {
+                               async createNew(): Promise<void> {
+                                       await this.open();
+                                       await 
browser.click("a.page-fab[routerLink='new']");
+                               },
+                               async open(): Promise<void> {
+                                       await browser.page.common()
+                                               .section.sidebar
+                                               .navigateToNode("servers", 
["serversContainer"]);
+                               },
+                               async openDetails(server: ResponseServer): 
Promise<void> {
+                                       await this.open();
+                                       const table = 
browser.page.servers.serversTable().section.serversTable;
+                                       await table
+                                               .filterTableByColumn("Host", 
server.hostName);
+                                       await table.doubleClickRow(1);
+                               },
                                ...TABLE_COMMANDS
                        } as ServersTableSectionCommands,
                        elements: {
                        },
-                       selector: "servers-table main"
+                       selector: "mat-card"
                }
        },
        url(): string {
@@ -50,6 +71,6 @@ type ServersTableSection = 
EnhancedSectionInstance<ServersTableSectionCommands,
  * The type of the servers table page object as provided by the Nightwatch API 
at
  * runtime.
  */
-export type ServersPageObject = EnhancedPageObject<{}, {}, { serversTable: 
ServersTableSection }>;
+export type ServersTablePageObject = EnhancedPageObject<{}, {}, { 
serversTable: ServersTableSection }>;
 
 export default serversPageObject;
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/statuses/statusDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/statuses/statusDetail.ts
index 133be79e6d..78358cdcaa 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/statuses/statusDetail.ts
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/statuses/statusDetail.ts
@@ -21,21 +21,11 @@ export type StatusDetailPageObject = EnhancedPageObject<{}, 
typeof statusDetailP
 
 const statusDetailPageObject = {
        elements: {
-               description: {
-                       selector: "input[name='description']"
-               },
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-               saveBtn: {
-                       selector: "button[type='submit']"
-               }
+               description: "input[name='description']",
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               name: "input[name='name']",
+               saveBtn: "button[type='submit']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/types/typeDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/types/typeDetail.ts
index 598025c0fb..29383bf717 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/types/typeDetail.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/types/typeDetail.ts
@@ -21,24 +21,12 @@ export type TypeDetailPageObject = EnhancedPageObject<{}, 
typeof typeDetailPageO
 
 const typeDetailPageObject = {
        elements: {
-               description: {
-                       selector: "input[name='description']"
-               },
-               id: {
-                       selector: "input[name='id']"
-               },
-               lastUpdated: {
-                       selector: "input[name='lastUpdated']"
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-               saveBtn: {
-                       selector: "button[type='submit']"
-               },
-               useInTable: {
-                       selector: "input[name='useInTable']"
-               }
+               description: "input[name='description']",
+               id: "input[name='id']",
+               lastUpdated: "input[name='lastUpdated']",
+               name: "input[name='name']",
+               saveBtn: "button[type='submit']",
+               useInTable: "input[name='useInTable']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/users/tenantDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/users/tenantDetail.ts
index dcc65a0880..3ea46f7eef 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/users/tenantDetail.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/users/tenantDetail.ts
@@ -21,18 +21,10 @@ export type TenantDetailPageObject = EnhancedPageObject<{}, 
typeof tenantDetailP
 
 const tenantDetailPageObject = {
        elements: {
-               active: {
-                       selector: "input[name='active']",
-               },
-               name: {
-                       selector: "input[name='name']"
-               },
-               parent: {
-                       selector: "input[name='parentTenant-tree-select']"
-               },
-               saveBtn: {
-                       selector: "button[type='submit']"
-               }
+               active: "input[name='active']",
+               name: "input[name='name']",
+               parent: "input[name='parentTenant-tree-select']",
+               saveBtn: "button[type='submit']",
        },
 };
 
diff --git 
a/experimental/traffic-portal/nightwatch/tests/servers/servers.detail.spec.ts 
b/experimental/traffic-portal/nightwatch/tests/servers/servers.detail.spec.ts
new file mode 100644
index 0000000000..c526f7708a
--- /dev/null
+++ 
b/experimental/traffic-portal/nightwatch/tests/servers/servers.detail.spec.ts
@@ -0,0 +1,101 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+describe("Servers Detail Spec", () => {
+       it("New server loads correctly", async () => {
+               await browser.page.servers.serversTable()
+                       .section.serversTable
+                       .createNew();
+               await browser.assert.urlContains("core/servers/new");
+               const page = browser.page.servers.serversDetail();
+               await page.section.detailCard
+                       .assert.enabled("@hostName")
+                       .assert.enabled("@cdn")
+                       .assert.enabled("@cacheGroup")
+                       .assert.enabled("@physLoc")
+                       .assert.enabled("@status")
+                       .assert.not.elementPresent("@offlineReason")
+                       .assert.enabled("@type")
+                       .assert.enabled("@httpPort")
+                       .assert.enabled("@httpsPort")
+                       .assert.enabled("@rack")
+                       .assert.not.elementPresent("@id")
+                       .assert.not.elementPresent("@lastUpdated")
+                       .assert.not.elementPresent("@statusLastUpdated")
+                       .assert.enabled("@profileNames")
+                       .assert.enabled("@intfAddBtn")
+                       .assert.enabled("@iloIP")
+                       .assert.enabled("@iloGateway")
+                       .assert.enabled("@iloNetmask")
+                       .assert.enabled("@iloUsername")
+                       .assert.enabled("@iloPassword")
+                       .assert.enabled("@mgmtIP")
+                       .assert.enabled("@mgmtGateway")
+                       .assert.enabled("@mgmtNetmask")
+                       .assert.not.elementPresent("@deleteBtn")
+                       .assert.enabled("@submitBtn");
+       });
+
+       it("Fields are loaded correctly", async () => {
+               await browser.page.servers.serversTable()
+                       .section.serversTable
+                       .openDetails(browser.globals.testData.edgeServer);
+               await browser.assert.urlContains(`core/servers/${  
browser.globals.testData.edgeServer.id}`);
+               const page = browser.page.servers.serversDetail();
+               await page.section.detailCard
+                       .assert.enabled("@hostName")
+                       .assert.valueEquals("@hostName", 
browser.globals.testData.edgeServer.hostName)
+                       .assert.enabled("@cdn")
+                       .assert.textEquals("@cdn", 
browser.globals.testData.edgeServer.cdnName)
+                       .assert.enabled("@cacheGroup")
+                       .assert.textEquals("@cacheGroup", 
browser.globals.testData.edgeServer.cachegroup)
+                       .assert.enabled("@physLoc")
+                       .assert.textEquals("@physLoc", 
browser.globals.testData.edgeServer.physLocation)
+                       .assert.not.enabled("@statusDisabled")
+                       .assert.valueEquals("@statusDisabled", 
browser.globals.testData.edgeServer.status)
+                       .assert.not.elementPresent("@offlineReason")
+                       .assert.enabled("@type")
+                       .assert.textEquals("@type", 
browser.globals.testData.edgeServer.type)
+                       .assert.enabled("@httpPort")
+                       .assert.enabled("@httpsPort")
+                       .assert.textEquals("@httpsPort", 
String(browser.globals.testData.edgeServer.httpsPort ?? ""))
+                       .assert.enabled("@rack")
+                       .assert.textEquals("@rack", 
browser.globals.testData.edgeServer.rack ?? "")
+                       .assert.not.enabled("@id")
+                       .assert.valueEquals("@id", 
String(browser.globals.testData.edgeServer.id))
+                       .assert.not.enabled("@lastUpdated")
+                       .assert.not.elementPresent("@statusLastUpdated")
+                       .assert.enabled("@profileNames")
+                       .assert.textEquals("@profileNames", 
browser.globals.testData.edgeServer.profileNames[0])
+                       .assert.enabled("@intfAddBtn")
+                       .assert.enabled("@iloIP")
+                       .assert.valueEquals("@iloIP", 
browser.globals.testData.edgeServer.iloIpAddress ?? "")
+                       .assert.enabled("@iloGateway")
+                       .assert.valueEquals("@iloGateway", 
browser.globals.testData.edgeServer.iloIpGateway ?? "" )
+                       .assert.enabled("@iloNetmask")
+                       .assert.valueEquals("@iloNetmask", 
browser.globals.testData.edgeServer.iloIpNetmask ?? "")
+                       .assert.enabled("@iloUsername")
+                       .assert.valueEquals("@iloUsername", 
browser.globals.testData.edgeServer.iloUsername ?? "")
+                       .assert.enabled("@iloPassword")
+                       .assert.valueEquals("@iloPassword", 
browser.globals.testData.edgeServer.iloPassword ?? "")
+                       .assert.enabled("@mgmtIP")
+                       .assert.valueEquals("@mgmtIP", 
browser.globals.testData.edgeServer.mgmtIpAddress ?? "")
+                       .assert.enabled("@mgmtGateway")
+                       .assert.valueEquals("@mgmtGateway", 
browser.globals.testData.edgeServer.mgmtIpGateway ?? "")
+                       .assert.enabled("@mgmtNetmask")
+                       .assert.valueEquals("@mgmtNetmask", 
browser.globals.testData.edgeServer.mgmtIpNetmask ?? "")
+                       .assert.enabled("@deleteBtn")
+                       .assert.enabled("@submitBtn");
+       });
+});
diff --git 
a/experimental/traffic-portal/nightwatch/tests/servers/servers.spec.ts 
b/experimental/traffic-portal/nightwatch/tests/servers/servers.table.spec.ts
similarity index 80%
rename from experimental/traffic-portal/nightwatch/tests/servers/servers.spec.ts
rename to 
experimental/traffic-portal/nightwatch/tests/servers/servers.table.spec.ts
index 62d0b66dc6..5715f4416a 100644
--- a/experimental/traffic-portal/nightwatch/tests/servers/servers.spec.ts
+++ b/experimental/traffic-portal/nightwatch/tests/servers/servers.table.spec.ts
@@ -12,13 +12,11 @@
 * limitations under the License.
 */
 
-describe("Servers Spec", () => {
+describe("Servers Table Spec", () => {
        it("Filter by hostname", async () => {
-               await browser.page.common()
-                       .section.sidebar
-                       .navigateToNode("servers", ["serversContainer"]);
+               const page = browser.page.servers.serversTable();
+               await page.section.serversTable.open();
                await browser.waitForElementPresent("input[name=fuzzControl]");
-               const page = browser.page.servers.servers();
                page.section.serversTable.searchText("edge");
                await page.assert.urlContains("search=edge");
        });
diff --git a/experimental/traffic-portal/nightwatch/tests/users/users.spec.ts 
b/experimental/traffic-portal/nightwatch/tests/users/users.spec.ts
index 16620206eb..8f701f88b4 100644
--- a/experimental/traffic-portal/nightwatch/tests/users/users.spec.ts
+++ b/experimental/traffic-portal/nightwatch/tests/users/users.spec.ts
@@ -24,7 +24,7 @@ describe("Users Spec", () => {
                await browser.waitForElementPresent(".ag-row");
                let tbl = page.section.usersTable;
                if (! await tbl.getColumnState("Username")) {
-                       tbl = tbl.toggleColumn("Username");
+                       await tbl.toggleColumn("Username");
                }
 
                tbl = tbl.searchText(username);
diff --git a/experimental/traffic-portal/package-lock.json 
b/experimental/traffic-portal/package-lock.json
index 9eeaf4f11e..3835924eea 100644
--- a/experimental/traffic-portal/package-lock.json
+++ b/experimental/traffic-portal/package-lock.json
@@ -31,7 +31,7 @@
         "chart.js": "^2.9.4",
         "express": "^4.15.2",
         "rxjs": "~6.6.0",
-        "trafficops-types": "4.0.7",
+        "trafficops-types": "^4.0.10",
         "tslib": "^2.0.0",
         "zone.js": "~0.11.4"
       },
@@ -74,7 +74,7 @@
         "typescript": "^4.9.5"
       },
       "engines": {
-        "node": ">=16.20.0"
+        "node": ">=16.14.0"
       },
       "optionalDependencies": {
         "@compodoc/compodoc": "^1.1.18"
@@ -18214,9 +18214,9 @@
       }
     },
     "node_modules/trafficops-types": {
-      "version": "4.0.7",
-      "resolved": 
"https://registry.npmjs.org/trafficops-types/-/trafficops-types-4.0.7.tgz";,
-      "integrity": 
"sha512-O5cMK33T/hVXgAC90zt/gIHZ1Yl2Lz+AS4MFcLb5s+TVWibKBB8z5mjoYeNxLBnI3xMrnEiQNqVa8Abis/AoIA=="
+      "version": "4.0.10",
+      "resolved": 
"https://registry.npmjs.org/trafficops-types/-/trafficops-types-4.0.10.tgz";,
+      "integrity": 
"sha512-/GW+PgT7Rg5WhGtbkJdvTIXLga68wx3dy9crmXmOrrB6sPVcX7vbrQ9TAxGqRbsC52ZbZSxahqsmZgQZv8KK3A=="
     },
     "node_modules/traverse": {
       "version": "0.6.7",
@@ -33479,9 +33479,9 @@
       }
     },
     "trafficops-types": {
-      "version": "4.0.7",
-      "resolved": 
"https://registry.npmjs.org/trafficops-types/-/trafficops-types-4.0.7.tgz";,
-      "integrity": 
"sha512-O5cMK33T/hVXgAC90zt/gIHZ1Yl2Lz+AS4MFcLb5s+TVWibKBB8z5mjoYeNxLBnI3xMrnEiQNqVa8Abis/AoIA=="
+      "version": "4.0.10",
+      "resolved": 
"https://registry.npmjs.org/trafficops-types/-/trafficops-types-4.0.10.tgz";,
+      "integrity": 
"sha512-/GW+PgT7Rg5WhGtbkJdvTIXLga68wx3dy9crmXmOrrB6sPVcX7vbrQ9TAxGqRbsC52ZbZSxahqsmZgQZv8KK3A=="
     },
     "traverse": {
       "version": "0.6.7",
diff --git a/experimental/traffic-portal/package.json 
b/experimental/traffic-portal/package.json
index 7e5db4322b..ee540b9490 100644
--- a/experimental/traffic-portal/package.json
+++ b/experimental/traffic-portal/package.json
@@ -24,7 +24,7 @@
     "Traffic Portal"
   ],
   "engines": {
-    "node": ">=16.20.0"
+    "node": ">=16.14.0"
   },
   "engineStrict": true,
   "scripts": {
@@ -71,7 +71,7 @@
     "chart.js": "^2.9.4",
     "express": "^4.15.2",
     "rxjs": "~6.6.0",
-    "trafficops-types": "4.0.7",
+    "trafficops-types": "^4.0.10",
     "tslib": "^2.0.0",
     "zone.js": "~0.11.4"
   },
diff --git a/experimental/traffic-portal/src/app/api/cdn.service.spec.ts 
b/experimental/traffic-portal/src/app/api/cdn.service.spec.ts
index d3875c2961..e332a7cf0a 100644
--- a/experimental/traffic-portal/src/app/api/cdn.service.spec.ts
+++ b/experimental/traffic-portal/src/app/api/cdn.service.spec.ts
@@ -129,6 +129,46 @@ describe("CDNService", () => {
                await expectAsync(responseP).toBeResolved();
        });
 
+       it("Queues Updates by CDN", async () => {
+               const resp = service.queueServerUpdates(cdn);
+               const req = 
httpTestingController.expectOne(`/api/${service.apiVersion}/cdns/${cdn.id}/queue_update`);
+               expect(req.request.method).toBe("POST");
+               expect(req.request.params.keys().length).toBe(0);
+               expect(req.request.body).toEqual({action: "queue"});
+               req.flush({response: { action: "queue", cdnId: cdn.id }});
+               await expectAsync(resp).toBeResolved();
+       });
+
+       it("Queues Updates by CDN ID", async () => {
+               const resp = service.queueServerUpdates(cdn.id);
+               const req = 
httpTestingController.expectOne(`/api/${service.apiVersion}/cdns/${cdn.id}/queue_update`);
+               expect(req.request.method).toBe("POST");
+               expect(req.request.params.keys().length).toBe(0);
+               expect(req.request.body).toEqual({action: "queue"});
+               req.flush({response: { action: "queue", cdnId: cdn.id }});
+               await expectAsync(resp).toBeResolved();
+       });
+
+       it("Dequeues Updates by CDN", async () => {
+               const resp = service.dequeueServerUpdates(cdn);
+               const req = 
httpTestingController.expectOne(`/api/${service.apiVersion}/cdns/${cdn.id}/queue_update`);
+               expect(req.request.method).toBe("POST");
+               expect(req.request.params.keys().length).toBe(0);
+               expect(req.request.body).toEqual({action: "dequeue"});
+               req.flush({response: { action: "dequeue", cdnId: cdn.id }});
+               await expectAsync(resp).toBeResolved();
+       });
+
+       it("Dequeues Updates by CDN ID", async () => {
+               const resp = service.dequeueServerUpdates(cdn.id);
+               const req = 
httpTestingController.expectOne(`/api/${service.apiVersion}/cdns/${cdn.id}/queue_update`);
+               expect(req.request.method).toBe("POST");
+               expect(req.request.params.keys().length).toBe(0);
+               expect(req.request.body).toEqual({action: "dequeue"});
+               req.flush({response: { action: "dequeue", cdnId: cdn.id }});
+               await expectAsync(resp).toBeResolved();
+       });
+
        afterEach(() => {
                httpTestingController.verify();
        });
diff --git a/experimental/traffic-portal/src/app/api/cdn.service.ts 
b/experimental/traffic-portal/src/app/api/cdn.service.ts
index 88ae3c5d30..799d973d01 100644
--- a/experimental/traffic-portal/src/app/api/cdn.service.ts
+++ b/experimental/traffic-portal/src/app/api/cdn.service.ts
@@ -13,7 +13,7 @@
 */
 import { HttpClient } from "@angular/common/http";
 import { Injectable } from "@angular/core";
-import type { RequestCDN, ResponseCDN } from "trafficops-types";
+import type { CDNQueueResponse, RequestCDN, ResponseCDN } from 
"trafficops-types";
 
 import { APIService } from "./base-api.service";
 
@@ -78,6 +78,30 @@ export class CDNService extends APIService {
                return this.post<ResponseCDN>("cdns", cdn).toPromise();
        }
 
+       /**
+        * Queue updates to servers by a CDN
+        *
+        * @param cdn The CDN or ID to queue from
+        */
+       public async queueServerUpdates(cdn: ResponseCDN | number): 
Promise<CDNQueueResponse> {
+               const id = typeof cdn === "number" ? cdn : cdn.id;
+               const path = `cdns/${id}/queue_update`;
+               const action = {action: "queue"};
+               return this.post<CDNQueueResponse>(path, action).toPromise();
+       }
+
+       /**
+        * Dequeue updates to servers by a CDN
+        *
+        * @param cdn The CDN or ID to dequeue from
+        */
+       public async dequeueServerUpdates(cdn: ResponseCDN | number): 
Promise<CDNQueueResponse> {
+               const id = typeof cdn === "number" ? cdn : cdn.id;
+               const path = `cdns/${id}/queue_update`;
+               const action = {action: "dequeue"};
+               return this.post<CDNQueueResponse>(path, action).toPromise();
+       }
+
        /**
         * Replaces an existing CDN with the provided new definition of a
         * CDN.
diff --git a/experimental/traffic-portal/src/app/api/server.service.spec.ts 
b/experimental/traffic-portal/src/app/api/server.service.spec.ts
index af84013733..f8c2973e58 100644
--- a/experimental/traffic-portal/src/app/api/server.service.spec.ts
+++ b/experimental/traffic-portal/src/app/api/server.service.spec.ts
@@ -14,13 +14,14 @@
  */
 import { HttpClientTestingModule, HttpTestingController } from 
"@angular/common/http/testing";
 import { TestBed } from "@angular/core/testing";
+import { type ResponseServer } from "trafficops-types";
 
 import { ServerService } from "./server.service";
 
 describe("ServerService", () => {
        let service: ServerService;
        let httpTestingController: HttpTestingController;
-       const server = {
+       const server: ResponseServer = {
                cachegroup: "cachegroup",
                cachegroupId: 1,
                cdnId: 1,
@@ -43,9 +44,7 @@ describe("ServerService", () => {
                offlineReason: null,
                physLocation: "physicalLocation",
                physLocationId: 1,
-               profile: "profile",
-               profileDesc: "profileDesc",
-               profileId: 1,
+               profileNames: ["profile"],
                rack: null,
                revalPending: false,
                routerHostName: null,
@@ -134,6 +133,46 @@ describe("ServerService", () => {
                        req.flush({response: server});
                        await expectAsync(responseP).toBeResolvedTo(server);
                });
+
+               it("updates a server by ID", async ()  => {
+                       const resp = service.updateServer(server.id, server);
+                       const req = 
httpTestingController.expectOne(`/api/${service.apiVersion}/servers/${server.id}`);
+                       expect(req.request.method).toBe("PUT");
+                       expect(req.request.body).toEqual(server);
+
+                       req.flush({response: server});
+                       await expectAsync(resp).toBeResolvedTo(server);
+               });
+
+               it("updates a server", async ()  => {
+                       const resp = service.updateServer(server);
+                       const req = 
httpTestingController.expectOne(`/api/${service.apiVersion}/servers/${server.id}`);
+                       expect(req.request.method).toBe("PUT");
+                       expect(req.request.body).toEqual(server);
+
+                       req.flush({response: server});
+                       await expectAsync(resp).toBeResolvedTo(server);
+               });
+
+               it("delete a server", async ()  => {
+                       const resp = service.deleteServer(server);
+                       const req = 
httpTestingController.expectOne(`/api/${service.apiVersion}/servers/${server.id}`);
+                       expect(req.request.method).toBe("DELETE");
+                       expect(req.request.body).toBeNull();
+
+                       req.flush({response: server});
+                       await expectAsync(resp).toBeResolvedTo(server);
+               });
+
+               it("delete a server by ID", async ()  => {
+                       const resp = service.deleteServer(server.id);
+                       const req = 
httpTestingController.expectOne(`/api/${service.apiVersion}/servers/${server.id}`);
+                       expect(req.request.method).toBe("DELETE");
+                       expect(req.request.body).toBeNull();
+
+                       req.flush({response: server});
+                       await expectAsync(resp).toBeResolvedTo(server);
+               });
        });
 
        describe("Status-related methods", () => {
diff --git a/experimental/traffic-portal/src/app/api/server.service.ts 
b/experimental/traffic-portal/src/app/api/server.service.ts
index 7126930085..7d1fa291d7 100644
--- a/experimental/traffic-portal/src/app/api/server.service.ts
+++ b/experimental/traffic-portal/src/app/api/server.service.ts
@@ -101,6 +101,29 @@ export class ServerService extends APIService {
                return this.post<ResponseServer>("servers", s).toPromise();
        }
 
+       /**
+        * Updates a server by the given payload
+        *
+        * @param serverOrID The server object or id to be deleted
+        * @param payload The server payload to update with.
+        */
+       public async updateServer(serverOrID: ResponseServer | number, 
payload?: RequestServer): Promise<ResponseServer> {
+               let id;
+               let body;
+               if (typeof(serverOrID) === "number") {
+                       if(!payload) {
+                               throw new TypeError("invalid call signature - 
missing request paylaod");
+                       }
+                       body = payload;
+                       id = +serverOrID;
+               } else {
+                       body = serverOrID;
+                       id = serverOrID.id;
+               }
+
+               return this.put<ResponseServer>(`servers/${id}`, 
body).toPromise();
+       }
+
        public async getServerChecks(): Promise<Servercheck[]>;
        public async getServerChecks(id: number): Promise<Servercheck>;
        /**
@@ -235,6 +258,19 @@ export class ServerService extends APIService {
                return 
this.delete<ResponseStatus>(`statuses/${id}`).toPromise();
        }
 
+       /**
+        * Deletes an existing server.
+        *
+        * @param server The Server to be deleted, or just its ID.
+        * @returns The deleted server.
+        */
+       public async deleteServer(server: number | ResponseServer): 
Promise<ResponseServer> {
+               const id =  typeof(server) === "number" ? server : server.id;
+               const path = `servers/${id}`;
+               return this.delete<ResponseServer>(path).toPromise();
+
+       }
+
        /**
         * Retrieves Server Capabilities from Traffic Ops.
         *
diff --git a/experimental/traffic-portal/src/app/api/testing/cdn.service.ts 
b/experimental/traffic-portal/src/app/api/testing/cdn.service.ts
index 2bcf2afa20..a93c5cc947 100644
--- a/experimental/traffic-portal/src/app/api/testing/cdn.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/cdn.service.ts
@@ -13,7 +13,7 @@
 */
 
 import { Injectable } from "@angular/core";
-import { RequestCDN, ResponseCDN } from "trafficops-types";
+import { CDNQueueResponse, RequestCDN, ResponseCDN } from "trafficops-types";
 
 /**
  * CDNService expose API functionality relating to CDNs.
@@ -88,6 +88,39 @@ export class CDNService {
                this.cdns.push(c);
                return c;
        }
+       /**
+        * Queue updates to servers by a CDN
+        *
+        * @param cdn The CDN or id to queue server updates for
+        */
+       public async queueServerUpdates(cdn: ResponseCDN | number): 
Promise<CDNQueueResponse> {
+               const id = typeof cdn === "number" ? cdn : cdn.id;
+               const realCDN = this.cdns.find(c => c.id === id);
+               if (!realCDN) {
+                       throw new Error(`No CDN ${id}`);
+               }
+               return {
+                       action: "queue",
+                       cdnId: id
+               };
+       }
+
+       /**
+        * Dequeue updates to servers by a CDN
+        *
+        * @param cdn The CDN or id to dequeue server updates for
+        */
+       public async dequeueServerUpdates(cdn: ResponseCDN | number): 
Promise<CDNQueueResponse> {
+               const id = typeof cdn === "number" ? cdn : cdn.id;
+               const realCDN = this.cdns.find(c => c.id === id);
+               if (!realCDN) {
+                       throw new Error(`No CDN ${id}`);
+               }
+               return {
+                       action: "dequeue",
+                       cdnId: id
+               };
+       }
 
        /**
         * Replaces an existing CDN with the provided new definition of a
diff --git a/experimental/traffic-portal/src/app/api/testing/server.service.ts 
b/experimental/traffic-portal/src/app/api/testing/server.service.ts
index d6b74f7f6f..bda7efb260 100644
--- a/experimental/traffic-portal/src/app/api/testing/server.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/server.service.ts
@@ -41,7 +41,7 @@ function serverCheck(server: ResponseServer): Servercheck {
                cacheGroup: server.cachegroup ?? "SERVER HAD NO CACHE GROUP",
                hostName: server.hostName ?? "SERVER HAD NO HOST NAME",
                id: server.id,
-               profile: server.profile ?? "SERVER HAD NO PROFILE",
+               profile: server.profileNames[0] ?? "SERVER HAD NO PROFILE",
                revalPending: server.revalPending,
                type: server.type ?? "SERVER HAD NO TYPE",
                updPending: server.updPending
@@ -146,7 +146,7 @@ export class ServerService {
        public async createServer(server: RequestServer): 
Promise<ResponseServer> {
                const cdn = await this.cdnService.getCDNs(server.cdnId);
                const physLoc = await 
this.physLocService.getPhysicalLocations(server.physLocationId);
-               const profile = await 
this.profileService.getProfiles(server.profileId);
+               const profile = await 
this.profileService.getProfiles(server.profileNames[0]);
                const type = await this.typeService.getTypes(server.typeId);
                const status = await this.getStatuses(server.statusId);
                const newServer = {
@@ -185,6 +185,33 @@ export class ServerService {
                return newServer;
        }
 
+       /**
+        * Updates a server by the given payload
+        *
+        * @param serverOrID The server object or id to be deleted
+        * @param payload The server payload to update with.
+        */
+       public async updateServer(serverOrID: ResponseServer | number, 
payload?: RequestServer): Promise<ResponseServer> {
+               let id: number;
+               let body: ResponseServer;
+               if (typeof(serverOrID) === "number") {
+                       if(!payload) {
+                               throw new TypeError("invalid call signature - 
missing request paylaod");
+                       }
+                       id = +serverOrID;
+                       body = payload as ResponseServer;
+               } else {
+                       id = serverOrID.id;
+                       body = serverOrID;
+               }
+               const index = this.servers.findIndex(s => s.id === id);
+               if (index < 0) {
+                       throw new Error(`Unknown server ${id}`);
+               }
+               this.servers[index] = body;
+               return this.servers[index];
+       }
+
        public async getServerChecks(): Promise<Servercheck[]>;
        public async getServerChecks(id: number): Promise<Servercheck>;
        /**
@@ -460,4 +487,21 @@ export class ServerService {
                this.capabilities.push(created);
                return created;
        }
+
+       /**
+        * Deletes an existing server.
+        *
+        * @param server The Server to be deleted, or just its ID.
+        * @returns The deleted server.
+        */
+       public async deleteServer(server: number | ResponseServer): 
Promise<ResponseServer> {
+               const id =  typeof(server) === "number" ? server : server.id;
+               const index = this.servers.findIndex(s => s.id === id);
+               if(index < 0) {
+                       throw new Error(`no such Server ${id}`);
+               }
+               const ret = this.servers[index];
+               this.servers.splice(index, 1);
+               return ret;
+       }
 }
diff --git a/experimental/traffic-portal/src/app/app.ui.module.ts 
b/experimental/traffic-portal/src/app/app.ui.module.ts
index f92c3310ec..714127ebd6 100644
--- a/experimental/traffic-portal/src/app/app.ui.module.ts
+++ b/experimental/traffic-portal/src/app/app.ui.module.ts
@@ -12,6 +12,7 @@
 * limitations under the License.
 */
 
+import { CdkDrag, CdkDropList } from "@angular/cdk/drag-drop";
 import { CdkMenuModule } from "@angular/cdk/menu";
 import { NgOptimizedImage } from "@angular/common";
 import { NgModule } from "@angular/core";
@@ -80,10 +81,14 @@ import { AgGridModule } from "ag-grid-angular";
                MatTooltipModule,
                MatTreeModule,
                MatSlideToggleModule,
-               NgOptimizedImage
+               NgOptimizedImage,
+               CdkDrag,
+               CdkDropList
        ],
        imports: [
-               NgOptimizedImage
+               NgOptimizedImage,
+               CdkDrag,
+               CdkDropList
        ]
 })
 export class AppUIModule {}
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts 
b/experimental/traffic-portal/src/app/core/core.module.ts
index 93fa833903..5f4b42d098 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -160,7 +160,7 @@ export const ROUTES: Routes = [
                SharedModule,
                AppUIModule,
                CommonModule,
-               RouterModule.forChild(ROUTES)
+               RouterModule.forChild(ROUTES),
        ]
 })
 export class CoreModule { }
diff --git 
a/experimental/traffic-portal/src/app/core/servers/server-details/_server-details-theme.scss
 
b/experimental/traffic-portal/src/app/core/servers/server-details/_server-details-theme.scss
index 586739854f..46c23c739f 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/server-details/_server-details-theme.scss
+++ 
b/experimental/traffic-portal/src/app/core/servers/server-details/_server-details-theme.scss
@@ -20,6 +20,11 @@
        $error: map.get($color, error);
        $background: map.get($color, background);
        $foreground: map.get($color, foreground);
+
+       .mat-expansion-panel-body {
+               padding: 0 12px 16px !important;
+       }
+
        tp-server-details form fieldset {
                color: mat.get-color-from-palette($foreground, text);
                background-color: mat.get-color-from-palette($background, 
background);
diff --git 
a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.html
 
b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.html
index 0bc3061f43..cd37ec7b54 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.html
+++ 
b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.html
@@ -11,17 +11,17 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<mat-card appearance="outlined">
+<mat-card appearance="outlined" class="page-content">
+       <mat-card-header class="actions-container" *ngIf="!isNew && server">
+               <button [disabled]="!isCache() || true" mat-button 
type="button">Manage Capabilities</button>
+               <button [disabled]="!isCache() || true" mat-button 
type="button">Manage Delivery Services</button>
+               <button [disabled]="!isCache()" mat-icon-button type="button" 
title="queue server updates" *ngIf="!server.updPending" 
(click)="queue()"><fa-icon [icon]="updateIcon"></fa-icon></button>
+               <button [disabled]="!isCache()" mat-icon-button type="button" 
title="clear server updates" *ngIf="server.updPending" 
(click)="dequeue()"><fa-icon [icon]="clearUpdatesIcon"></fa-icon></button>
+               <button type="button" mat-icon-button title="change server 
status" (click)="changeStatus($event)"><fa-icon 
[icon]="statusChangeIcon"></fa-icon></button>
+       </mat-card-header>
        <tp-loading *ngIf="!server"></tp-loading>
-       <mat-card-content *ngIf="server">
-               <div class="actions-container" *ngIf="!isNew">
-                       <button [disabled]="!isCache()" mat-button 
type="button">Manage Capabilities</button>
-                       <button [disabled]="!isCache()" mat-button 
type="button">Manage Delivery Services</button>
-                       <button [disabled]="!isCache()" mat-icon-button 
type="button" title="queue server updates" *ngIf="!server.updPending"><fa-icon 
[icon]="updateIcon"></fa-icon></button>
-                       <button [disabled]="!isCache()" mat-icon-button 
type="button" title="clear server updates" *ngIf="server.updPending"><fa-icon 
[icon]="clearUpdatesIcon"></fa-icon></button>
-                       <button type="button" mat-icon-button title="change 
server status" (click)="changeStatus($event)"><fa-icon 
[icon]="statusChangeIcon"></fa-icon></button>
-               </div>
-               <form ngNativeValidate #serverForm="ngForm" 
(ngSubmit)="submit($event)">
+       <form ngNativeValidate #serverForm="ngForm" 
class="triple-column-responsive" (ngSubmit)="submit($event)">
+               <mat-card-content *ngIf="server" class="container">
                        <mat-form-field>
                                <mat-label>Host Name</mat-label>
                                <input matInput [(ngModel)]="server.hostName" 
name="hostname" required maxlength="50"/>
@@ -35,24 +35,32 @@ limitations under the License.
                                <mat-select name="cdn" 
[(ngModel)]="server.cdnId" required>
                                        <mat-option *ngFor="let cdn of cdns" 
[value]="cdn.id">{{cdn.name}}</mat-option>
                                </mat-select>
+                               <mat-hint>
+                                       <a mat-icon-button 
[disabled]="!server.cdnId" class="small-icon-button" matTooltip="View CDN 
Details" aria-label="View CDN Details" color="primary" 
[routerLink]="'/core/cdns/' + server.cdnId" target="_blank">
+                                               <mat-icon>link</mat-icon>
+                                       </a>
+                               </mat-hint>
                        </mat-form-field>
                        <mat-form-field>
                                <mat-label>Cache Group</mat-label>
                                <mat-select name="cachegroup" 
[(ngModel)]="server.cachegroupId" required>
                                        <mat-option *ngFor="let cg of 
cacheGroups" [value]="cg.id">{{cg.name}}</mat-option>
                                </mat-select>
-                       </mat-form-field>
-                       <mat-form-field>
-                               <mat-label>Profile</mat-label>
-                               <mat-select name="profile" 
[(ngModel)]="server.profileId" required>
-                                       <mat-option *ngFor="let profile of 
profiles" [value]="profile.id">{{profile.name}}</mat-option>
-                               </mat-select>
+                               <mat-hint>
+                                       <a mat-icon-button 
[disabled]="!server.cachegroupId" class="small-icon-button" matTooltip="View 
Cache Group Details" aria-label="View Cache Group Details" color="primary" 
[href]="'/core/cache-groups/' + server.cachegroupId" target="_blank">
+                                               <mat-icon>link</mat-icon>
+                                       </a>
+                               </mat-hint>
                        </mat-form-field>
                        <mat-form-field>
                                <mat-label>Physical Location</mat-label>
                                <mat-select name="physLocation" 
[(ngModel)]="server.physLocationId" required>
                                        <mat-option *ngFor="let physLocation of 
physicalLocations" [value]="physLocation.id">{{physLocation.name}}</mat-option>
                                </mat-select>
+                               <mat-hint>
+                                       <a mat-icon-button 
[disabled]="!server.physLocationId" class="small-icon-button" matTooltip="View 
Physical Location Details" aria-label="View Physical Location Details" 
color="primary" [href]="'/core/phys-locs/' + server.physLocationId" 
target="_blank">
+                                               <mat-icon>link</mat-icon>
+                                       </a></mat-hint>
                        </mat-form-field>
                        <mat-form-field>
                                <mat-label>Status</mat-label>
@@ -70,6 +78,10 @@ limitations under the License.
                                <mat-select name="type" 
[(ngModel)]="server.typeId" required>
                                        <mat-option *ngFor="let type of types" 
[value]="type.id">{{type.name}}</mat-option>
                                </mat-select>
+                               <mat-hint>
+                                       <a mat-icon-button 
[disabled]="!server.typeId" class="small-icon-button" color="primary" 
aria-label="View Type Details" matTooltip="View Type Details" 
[href]="'/core/types/' + server.typeId" target="_blank">
+                                               <mat-icon>link</mat-icon>
+                                       </a></mat-hint>
                        </mat-form-field>
                        <div>
                                <mat-form-field>
@@ -91,7 +103,7 @@ limitations under the License.
                        </mat-form-field>
                        <mat-form-field *ngIf="!isNew && server.lastUpdated">
                                <mat-label>Last Updated</mat-label>
-                               <input matInput 
[value]="server.lastUpdated.toLocaleString()" disabled/>
+                               <input matInput name="lastUpdated" 
[value]="server.lastUpdated.toLocaleString()" disabled/>
                        </mat-form-field>
                        <mat-form-field *ngIf="!isNew">
                                <mat-label>Hash ID</mat-label>
@@ -101,96 +113,159 @@ limitations under the License.
                                <mat-label>Status Last Updated</mat-label>
                                <input matInput name="statusLastUpdated" 
disabled [value]="server.statusLastUpdated.toLocaleString()"/>
                        </mat-form-field>
-                       <fieldset>
-                               <legend 
(click)="hideInterfaces=!hideInterfaces">Interfaces<button 
name="addInterfaceBtn" class="add-button" type="button" title="add a new 
interface" (click)="addInterface($event)"><fa-icon 
[icon]="addIcon"></fa-icon></button></legend>
-                               <fieldset [hidden]="hideInterfaces" *ngFor="let 
inf of server.interfaces; index as infInd">
-                                       <legend>
-                                               <label 
for="{{inf.name}}-name">Name</label>
-                                               <input [(ngModel)]="inf.name" 
required aria-label="Interface name" id="{{inf.name}}-name" 
name="{{inf.name}}-name" class="interface-name-input"/>
-                                               <button class="remove-button" 
type="button" title="delete this interface" 
(click)="deleteInterface(infInd)"><fa-icon 
[icon]="removeIcon"></fa-icon></button>
-                                       </legend>
-
-                                       <div>
-                                               <mat-checkbox 
[labelPosition]="'before'" id="{{inf.name}}-monitor" 
name="{{inf.name}}-monitor" [(ngModel)]="inf.monitor">Monitor this 
interface</mat-checkbox>
-                                               <mat-form-field>
-                                                       <mat-label><abbr 
title="Maximum Transmission Unit">MTU</abbr></mat-label>
-                                                       <input matInput 
id="{{inf.name}}-mtu" name="{{inf.name}}-mtu" type="number" min="1500" 
max="9000" step="7500" [(ngModel)]="inf.mtu"/>
-                                               </mat-form-field>
-                                               <!-- <small class="input-error" 
ng-show="hasPropertyError(serverForm[inf.name+'-mtu'], 'min') || 
hasPropertyError(serverForm[inf.name+'-mtu'], 'max') || 
hasPropertyError(serverForm[inf.name+'-mtu'], 'step')">Invalid MTU - must be 
1500 or 9000</small> -->
-                                               <mat-form-field>
-                                                       <mat-label>Maximum 
Bandwidth</mat-label>
-                                                       <input matInput 
id="{{inf.name}}-max-bandwidth" [(ngModel)]="inf.maxBandwidth" min="0" 
type="number" name="{{inf.name}}-max-bandwidth"/>
-                                                       <mat-hint 
class="input-warning" *ngIf="inf.maxBandwidth !== 0">Setting Max Bandwidth to 
zero will cause cache servers to always be unavailable</mat-hint>
-                                               </mat-form-field>
-                                               <!-- <small class="input-error" 
ng-show="hasPropertyError(serverForm[inf.name+'-max-bandwidth'], 'min')">Cannot 
be negative</small> -->
+                       <mat-expansion-panel [expanded]="true">
+                               <mat-expansion-panel-header>
+                                       <mat-panel-title>
+                                               Profiles
+                                       </mat-panel-title>
+                               </mat-expansion-panel-header>
+                               <div class="expansion-content-profile">
+                                       <mat-form-field>
+                                               <mat-label>Profiles</mat-label>
+                                               <mat-select name="profiles" 
[(ngModel)]="server.profileNames" multiple required>
+                                                       <mat-option *ngFor="let 
profile of profiles"  [value]="profile.name">{{profile.name}}</mat-option>
+                                               </mat-select>
+                                       </mat-form-field>
+                                       <div class="profile-order">
+                                               Ordering
+                                               <mat-list class="drop-list 
mat-elevation-z3" cdkDropList (cdkDropListDropped)="drop($event)">
+                                                       <mat-list-item 
class="drop-list-item mat-elevation-z1" *ngFor="let profile of 
server.profileNames; index as i" cdkDrag>
+                                                               {{i+1}}: 
{{profile}}
+                                                               <a 
class="small-icon-button" aria-label="View Profile Details" matTooltip="View 
Profile Details" color="primary" mat-icon-button [href]="'/core/profiles/' + 
profileNameToId(profile)" target="_blank">
+                                                                       
<mat-icon>link</mat-icon>
+                                                               </a>
+                                                       </mat-list-item>
+                                               </mat-list>
                                        </div>
-
-                                       <fieldset>
-                                               <legend>IP Addresses<button 
name="addIPBtn" type="button" title="add new IP Address" class="add-button" 
(click)="addIP(inf)"><fa-icon [icon]="addIcon"></fa-icon></button></legend>
-                                               <div *ngFor="let ip of 
inf.ipAddresses; index as ipInd" [ngClass]="{'bordered-item': ipInd !== 0}">
-                                                       <mat-checkbox 
[labelPosition]="'before'" name="{{inf.name}}-{{ip.address}}-serviceAddress" 
id="{{inf.name}}-{{ip.address}}-serviceAddress" class="service-addr-cb" 
[(ngModel)]="ip.serviceAddress">Is a Service Address</mat-checkbox>
-                                                       <mat-form-field>
-                                                               
<mat-label>Address</mat-label>
-                                                               <input matInput 
id="{{inf.name}}-{{ip.address}}" name="{{inf.name}}-{{ip.address}}" 
class="ip-input" [(ngModel)]="ip.address" [pattern]="validIPPattern" required 
placeholder="192.0.2.0/27" />
-                                                       </mat-form-field>
-                                                       <!-- <small 
class="input-error" 
ng-show="hasPropertyError(serverForm[inf.name+'-'+ip.address], 
'pattern')">Invalid address</small>
-                                                       <small 
class="input-error" 
ng-show="hasPropertyError(serverForm[inf.name+'-'+ip.address], 
'required')">Required</small>
-                                                       <small 
class="input-warning" ng-show="isLargeCIDR(ip.address)">Large CIDR detected. 
IPv4 with CIDR &lt; 27 or IPv6 with CIDR &lt; 64 can be problematic.</small> -->
-                                                       <mat-form-field>
-                                                               
<mat-label>Gateway</mat-label>
-                                                               <input matInput 
id="{{inf.name}}-{{ip.address}}-gateway" 
name="{{inf.name}}-{{ip.address}}-gateway" [(ngModel)]="ip.gateway" 
[pattern]="validGatewayPattern"/>
-                                                       </mat-form-field>
-                                                       <!-- <small 
class="input-error" 
ng-show="hasPropertyError(serverForm[inf.name+'-'+ip.address+'-gateway'], 
'pattern')">Invalid gateway</small> -->
-                                                       <button type="button" 
title="delete this IP address" class="remove-button" style="justify-self: 
start;" (click)="deleteIP(inf, ipInd)"><fa-icon 
[icon]="removeIcon"></fa-icon></button>
-                                               </div>
-                                       </fieldset>
-                               </fieldset>
-                       </fieldset>
-                       <fieldset>
-                               <legend (click)="hideILO=!hideILO"><abbr 
title="Integrated Lights-Out Management">ILO</abbr> Details</legend>
-                               <div [hidden]="hideILO">
+                               </div>
+                       </mat-expansion-panel>
+                       <mat-expansion-panel [expanded]="true">
+                               <mat-expansion-panel-header>
+                                       <mat-panel-title>
+                                               Interfaces
+                                       </mat-panel-title>
+                                       <mat-panel-description 
class="expansion-description">
+                                               <button aria-label="Add An 
Interface" class="panel-button" color="primary" mat-icon-button type="button" 
(click)="addInterface($event)">
+                                                       <mat-icon>add</mat-icon>
+                                               </button>
+                                       </mat-panel-description>
+                               </mat-expansion-panel-header>
+                               <div class="expansion-container">
+                                       <mat-accordion multi>
+                                               <mat-expansion-panel 
*ngFor="let inf of server.interfaces; index as infInd" [expanded]="true">
+                                                       
<mat-expansion-panel-header>
+                                                               
<mat-panel-title>
+                                                                       
{{getInterfaceName(inf)}}
+                                                               
</mat-panel-title>
+                                                               
<mat-panel-description class="expansion-description">
+                                                                       <button 
class="panel-button" color="warn" mat-icon-button type="button" 
(click)="deleteInterface($event, infInd)">
+                                                                               
<mat-icon>delete</mat-icon>
+                                                                       
</button>
+                                                               
</mat-panel-description>
+                                                       
</mat-expansion-panel-header>
+                                                       <div 
class="expansion-content">
+                                                               <mat-form-field>
+                                                                       
<mat-label>Name</mat-label>
+                                                                       <input 
matInput id="{{inf.name}}-name" name="{{inf.name}}-name" [(ngModel)]="inf.name" 
/>
+                                                               
</mat-form-field>
+                                                               <mat-form-field>
+                                                                       
<mat-label><abbr title="Maximum Transmission Unit">MTU</abbr></mat-label>
+                                                                       <input 
matInput id="{{inf.name}}-mtu" name="{{inf.name}}-mtu" type="number" min="1500" 
max="9000" step="7500" [(ngModel)]="inf.mtu"/>
+                                                               
</mat-form-field>
+                                                               <mat-form-field>
+                                                                       
<mat-label>Maximum Bandwidth</mat-label>
+                                                                       <input 
matInput id="{{inf.name}}-max-bandwidth" [(ngModel)]="inf.maxBandwidth" min="0" 
type="number" name="{{inf.name}}-max-bandwidth"/>
+                                                                       
<mat-hint class="input-warning" *ngIf="inf.maxBandwidth !== 0">Cache servers 
will be unavailable</mat-hint>
+                                                               
</mat-form-field>
+                                                               <mat-checkbox 
[labelPosition]="'before'" id="{{inf.name}}-monitor" 
name="{{inf.name}}-monitor" [(ngModel)]="inf.monitor">Monitor this 
interface</mat-checkbox>
+                                                       </div>
+                                                       <div 
class="expansion-container">
+                                                               
<mat-expansion-panel [expanded]="true">
+                                                                       
<mat-expansion-panel-header>
+                                                                               
<mat-panel-title>
+                                                                               
        IP Addresses
+                                                                               
</mat-panel-title>
+                                                                               
<mat-panel-description class="expansion-description">
+                                                                               
        <button class="panel-button" color="primary" mat-icon-button 
type="button" (click)="addIP($event, inf)">
+                                                                               
                <mat-icon>add</mat-icon>
+                                                                               
        </button>
+                                                                               
</mat-panel-description>
+                                                                       
</mat-expansion-panel-header>
+                                                                       <div 
class="expansion-ip-content" *ngFor="let ip of inf.ipAddresses; index as ipInd">
+                                                                               
<mat-checkbox [labelPosition]="'before'" 
name="{{inf.name}}-{{ip.address}}-serviceAddress" 
id="{{inf.name}}-{{ip.address}}-serviceAddress" class="service-addr-cb" 
[(ngModel)]="ip.serviceAddress">Is a Service Address</mat-checkbox>
+                                                                               
<mat-form-field>
+                                                                               
        <mat-label>Address</mat-label>
+                                                                               
        <input matInput id="{{inf.name}}-{{ip.address}}" 
name="{{inf.name}}-{{ip.address}}" class="ip-input" [(ngModel)]="ip.address" 
[pattern]="validIPPattern" required placeholder="192.0.2.0/27" />
+                                                                               
</mat-form-field>
+                                                                               
<mat-form-field>
+                                                                               
        <mat-label>Gateway</mat-label>
+                                                                               
        <input matInput id="{{inf.name}}-{{ip.address}}-gateway" 
name="{{inf.name}}-{{ip.address}}-gateway" [(ngModel)]="ip.gateway" 
[pattern]="validGatewayPattern"/>
+                                                                               
</mat-form-field>
+                                                                               
<button mat-icon-button type="button" color="warn" title="delete this IP 
address" class="remove-button" (click)="deleteIP($event, inf, 
ipInd)"><mat-icon>delete</mat-icon></button>
+                                                                       </div>
+                                                               
</mat-expansion-panel>
+                                                       </div>
+                                               </mat-expansion-panel>
+                                       </mat-accordion>
+                               </div>
+                       </mat-expansion-panel>
+                       <mat-expansion-panel [expanded]="true">
+                               <mat-expansion-panel-header>
+                                       <mat-panel-title>
+                                               <p><abbr title="Integrated 
Lights-Out Management">ILO</abbr> Details</p>
+                                       </mat-panel-title>
+                               </mat-expansion-panel-header>
+                               <div class="expansion-content">
                                        <mat-form-field>
-                                               <mat-label><abbr 
title="Integrated Lights-Out Management">ILO</abbr> IP Address</mat-label>
+                                               <mat-label>IP 
Address</mat-label>
                                                <input matInput name="iloIP" 
[(ngModel)]="server.iloIpAddress"/>
                                        </mat-form-field>
                                        <mat-form-field>
-                                               <mat-label><abbr 
title="Integrated Lights-Out Management">ILO</abbr> Gateway IP 
Address</mat-label>
+                                               <mat-label>Gateway IP 
Address</mat-label>
                                                <input matInput 
name="iloGateway" [(ngModel)]="server.iloIpGateway"/>
                                        </mat-form-field>
                                        <mat-form-field>
-                                               <mat-label><abbr 
title="Integrated Lights-Out Management">ILO</abbr> IP Netmask</mat-label>
+                                               <mat-label>IP 
Netmask</mat-label>
                                                <input matInput 
name="iloNetmask" [(ngModel)]="server.iloIpNetmask"/>
                                        </mat-form-field>
                                        <mat-form-field>
-                                               <mat-label><abbr 
title="Integrated Lights-Out Management">ILO</abbr> Username</mat-label>
+                                               <mat-label>Username</mat-label>
                                                <input matInput 
name="iloUsername" [(ngModel)]="server.iloUsername"/>
                                        </mat-form-field>
                                        <mat-form-field>
-                                               <mat-label><abbr 
title="Integrated Lights-Out Management">ILO</abbr> Password</mat-label>
+                                               <mat-label>Password</mat-label>
                                                <tp-obscured-text-input 
name="iloPassword" [autocomplete]="autocompleteNew" 
[(value)]="server.iloPassword"></tp-obscured-text-input>
                                        </mat-form-field>
                                </div>
-                       </fieldset>
-                       <fieldset>
-                               <legend 
(click)="hideManagement=!hideManagement">Management Interface Details</legend>
-                               <div [hidden]="hideManagement">
+                       </mat-expansion-panel>
+                       <mat-expansion-panel [expanded]="true">
+                               <mat-expansion-panel-header>
+                                       <mat-panel-title>
+                                               <p>Management Interface 
Details</p>
+                                       </mat-panel-title>
+                               </mat-expansion-panel-header>
+                               <div class="expansion-content">
                                        <mat-form-field>
-                                               <mat-label>Management IP 
Address</mat-label>
+                                               <mat-label>IP 
Address</mat-label>
                                                <input matInput name="mgmtIP" 
[(ngModel)]="server.mgmtIpAddress"/>
+                                               <mat-icon matSuffix 
[matTooltipClass]="'multi-line-tooltip'" [matTooltip]="'The IP Address for the 
server\'s management interface\n\nDeprecated:\nThis field has been deprecated 
and will be removed in a future version of Traffic 
Control'">help_outline</mat-icon>
                                        </mat-form-field>
                                        <mat-form-field>
-                                               <mat-label>Management Gateway 
IP Address</mat-label>
+                                               <mat-label>Gateway IP 
Address</mat-label>
                                                <input matInput 
name="mgmtIpGateway" [(ngModel)]="server.mgmtIpGateway"/>
+                                               <mat-icon matSuffix 
[matTooltipClass]="'multi-line-tooltip'" [matTooltip]="'The IPv4 Gateway for 
the server\'s management interface\n\nDeprecated:\nThis field has been 
deprecated and will be removed in a future version of Traffic 
Control'">help_outline</mat-icon>
                                        </mat-form-field>
                                        <mat-form-field>
-                                               <mat-label>Management IP 
Netmask</mat-label>
+                                               <mat-label>IP 
Netmask</mat-label>
                                                <input matInput 
name="mgmtIpNetmask" [(ngModel)]="server.mgmtIpNetmask"/>
+                                               <mat-icon matSuffix 
[matTooltipClass]="'multi-line-tooltip'" [matTooltip]="'The IPv4 Netmask for 
the server\'s management interface\n\nDeprecated:\nThis field has been 
deprecated and will be removed in a future version of Traffic 
Control'">help_outline</mat-icon>
                                        </mat-form-field>
                                </div>
-                       </fieldset>
-                       <div class="buttons">
-                               <button mat-raised-button 
type="submit">Submit</button>
-                       </div>
-               </form>
-       </mat-card-content>
+                       </mat-expansion-panel>
+               </mat-card-content>
+               <mat-card-actions align="end">
+                       <button mat-raised-button type="button" 
aria-label="Delete Server" *ngIf="!isNew" color="warn" 
(click)="delete()">Delete</button>
+                       <button mat-raised-button type="submit" 
aria-label="Submit Server" color="primary">Submit</button>
+               </mat-card-actions>
+       </form>
 </mat-card>
diff --git 
a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.scss
 
b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.scss
index 577cb5a1ae..98fb894d50 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.scss
+++ 
b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.scss
@@ -11,139 +11,120 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-
-.mat-mdc-card {
-       width: fit-content;
-       margin: 15px auto auto;
-       min-width: 685px;
-}
+@import "../../../../styles/vars";
 
 .actions-container {
-       width: 85vw;
-       margin: 25px auto 0;
-       display: flex;
        justify-content: flex-end;
-       padding-right: 1em;
 
-       button {
-               border: none;
-               background: none;
-               font-size: large;
+       button[mat-icon-button] {
+               padding: 0;
+               height: 36px;
        }
 }
 
-form {
-       display: grid;
-       grid-template-columns: 1fr 1fr 1fr;
-       grid-column-gap: 15px;
-       grid-row-gap: 1em;
-       width: 85vw;
-       margin: 1em auto 50px;
-
-       small {
-               grid-column: 2;
-               justify-self: start;
+.drop-list-item {
+       .small-icon-button {
+               float: right;
+       }
+}
 
-               &.input-warning {
-                       border-radius: 4px;
+@include small-icon-button();
 
-                       &::before {
-                               content: "⚠";
-                       }
-               }
+form {
+       .mat-mdc-card-actions .mdc-button {
+               margin: 0 8px;
        }
 
-       output {
-               border: 1px solid darkgray;
-               padding: 2px 3px;
-               background-color: white;
-               display: inline-block;
-               appearance: textfield;
-               background-color: -moz-field;
-               box-shadow: 1px 1px 1px 0 lightgray inset;
-               font: -moz-field;
-               font: -webkit-small-control;
+       .cdk-drag-preview {
+               box-sizing: border-box;
+               border-radius: 4px;
+               box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
+               0 8px 10px 1px rgba(0, 0, 0, 0.14),
+               0 3px 14px 2px rgba(0, 0, 0, 0.12);
        }
 
-       legend {
-               font-size: 14px;
-               font-weight: bold;
-               margin-bottom: 0px;
-               border: 1px solid #ddd;
-               border-radius: 4px;
-               padding: 5px 5px 5px 10px;
-               width: 99%;
-               user-select: none;
+       .cdk-drag-placeholder {
+               opacity: 0;
        }
 
-       div {
-               grid-template-columns: 1fr 1fr;
-               grid-column-gap: 5px;
-               display: grid;
+       .cdk-drag-animating {
+               transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
        }
 
-       fieldset {
-               border: 1px solid #ddd;
-               border-radius: 4px;
+       .mat-expansion-panel {
                grid-column: 1 / -1;
 
-               button {
-                       float: right;
-                       color: white;
-                       padding: 1px 5px;
-                       font-size: 12px;
-                       line-height: 1.5;
-                       border-radius: 3px;
-                       text-align: center;
+               .expansion-container {
+                       display: grid;
+                       grid-template-columns: 1fr;
                }
 
-               fieldset {
-                       margin-top: 10px;
-
-                       div {
-                               grid-template-columns: auto 1fr 1fr auto;
-                       }
-
-                       input {
-                               margin-left: 5px;
-                       }
+               .expansion-content {
+                       display: grid;
+                       grid-template-columns: 1fr 1fr 1fr;
+                       grid-column-gap: 15px;
+               }
 
-                       .mat-mdc-checkbox {
-                               margin: auto;
-                       }
+               .expansion-ip-content {
+                       display: grid;
+                       grid-template-columns: auto 1fr 1fr auto;
+                       grid-column-gap: 10px;
                }
 
-               legend {
-                       cursor: pointer;
+               .expansion-description {
+                       justify-content: end;
                }
 
-               li {
+               .expansion-content-profile {
                        display: grid;
-                       grid-template-columns: auto 1fr;
-                       grid-column-gap: 10px;
-                       grid-row-gap: 0.75em;
+                       grid-template-columns: 1fr 1fr;
+                       grid-column-gap: 15px;
+
+                       .mat-mdc-form-field {
+                               height: fit-content;
+                       }
 
-                       &.bordered-item {
-                               border-top: 1px solid gray;
-                               margin-top: 10px;
-                               padding-top: 10px;
+                       div.profile-order {
+                               display: grid;
+                               grid-template-columns: 1fr;
+
+                               .drop-list {
+                                       display: grid;
+                                       grid-template-columns: 1fr;
+                                       padding: 0;
+
+                                       &:last-child {
+                                               border: none;
+                                               box-shadow: none;
+                                       }
+
+                                       .drop-list.cdk-drop-list-dragging 
.drop-list-item:not(.cdk-drag-placeholder) {
+                                               transition: transform 250ms 
cubic-bezier(0, 0, 0.2, 1);
+                                       }
+                               }
                        }
                }
+       }
 
-               div {
-                       width: 100%;
-                       display: grid;
-                       grid-template-columns: 1fr 1fr 1fr;
-                       grid-column-gap: 15px;
-                       grid-row-gap: 1em;
+       small {
+               grid-column: 2;
+               justify-self: start;
+
+               &.input-warning {
+                       border-radius: 4px;
 
-                       &[hidden] {
-                               display: none;
-                               visibility: hidden;
+                       &::before {
+                               content: "⚠";
                        }
                }
        }
 
+       div {
+               grid-template-columns: 1fr 1fr;
+               grid-column-gap: 5px;
+               display: grid;
+       }
+
        .buttons {
                grid-column-end: -1;
                justify-self: end;
@@ -160,7 +141,11 @@ form {
        form {
                grid-template-columns: 1fr;
 
-               fieldset div,  {
+               .mat-expansion-panel .expansion-content-profile {
+                       grid-template-columns: 1fr;
+               }
+
+               fieldset div, .mat-expansion-panel .expansion-content {
                        grid-template-columns: 1fr 1fr;
                }
        }
diff --git 
a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.spec.ts
index 6aa18221fc..685c8a831a 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.spec.ts
+++ 
b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.spec.ts
@@ -15,6 +15,7 @@
 import { HttpClientModule } from "@angular/common/http";
 import { type ComponentFixture, fakeAsync, TestBed, tick } from 
"@angular/core/testing";
 import { FormsModule, ReactiveFormsModule } from "@angular/forms";
+import { MatDialog, MatDialogRef } from "@angular/material/dialog";
 import { MatFormFieldModule } from "@angular/material/form-field";
 import { MatInputModule } from "@angular/material/input";
 import { MatSelectModule } from "@angular/material/select";
@@ -43,7 +44,7 @@ describe("ServerDetailsComponent", () => {
                                HttpClientModule,
                                RouterTestingModule.withRoutes([
                                        {component: ServerDetailsComponent, 
path: "server/:id"},
-                                       {component: ServerDetailsComponent, 
path: "server/new"}
+                                       {component: ServerDetailsComponent, 
path: "server/new"},
                                ]),
                                FormsModule,
                                ReactiveFormsModule,
@@ -78,7 +79,7 @@ describe("ServerDetailsComponent", () => {
                        mgmtIpNetmask: null,
                        offlineReason: null,
                        physLocationId: 1,
-                       profileId: 1,
+                       profileNames: ["GLOBAL"],
                        statusId: 1,
                        typeId: 1
                });
@@ -106,22 +107,22 @@ describe("ServerDetailsComponent", () => {
                expect(component.server.interfaces.length).toBe(1);
                component.addInterface(new MouseEvent("click"));
                expect(component.server.interfaces.length).toBe(2);
-               component.deleteInterface(1);
+               component.deleteInterface(new MouseEvent("click"), 1);
                expect(component.server.interfaces.length).toBe(1);
-               component.deleteInterface(0);
+               component.deleteInterface(new MouseEvent("click"), 0);
                expect(component.server.interfaces.length).toBe(0);
        });
 
        it("adds and removes IP addresses to/from an interface", () => {
                component.addInterface(new MouseEvent("click"));
                
expect(component.server.interfaces[0].ipAddresses.length).toBe(0);
-               component.addIP(component.server.interfaces[0]);
+               component.addIP(new MouseEvent("click"), 
component.server.interfaces[0]);
                
expect(component.server.interfaces[0].ipAddresses.length).toBe(1);
-               component.addIP(component.server.interfaces[0]);
+               component.addIP(new MouseEvent("click"), 
component.server.interfaces[0]);
                
expect(component.server.interfaces[0].ipAddresses.length).toBe(2);
-               component.deleteIP(component.server.interfaces[0], 1);
+               component.deleteIP(new MouseEvent("click"), 
component.server.interfaces[0], 1);
                
expect(component.server.interfaces[0].ipAddresses.length).toBe(1);
-               component.deleteIP(component.server.interfaces[0], 0);
+               component.deleteIP(new MouseEvent("click"), 
component.server.interfaces[0], 0);
                
expect(component.server.interfaces[0].ipAddresses.length).toBe(0);
        });
 
@@ -157,16 +158,15 @@ describe("ServerDetailsComponent", () => {
        }));
 
        it("opens the 'change status' dialog", () => {
-               expect(component.changeStatusDialogOpen).toBeFalse();
-               component.changeStatus(new MouseEvent("click"));
-               expect(component.changeStatusDialogOpen).toBeTrue();
+               const mockMatDialog = TestBed.inject(MatDialog);
+               const openSpy = spyOn(mockMatDialog, "open").and.returnValue({
+                       afterClosed: () => of(true)
+               } as MatDialogRef<unknown>);
                component.isNew = true;
                expect(() => component.changeStatus(new 
MouseEvent("click"))).toThrow();
-       });
-
-       it("closes the 'change status' dialog when done", () => {
-               component.changeStatusDialogOpen = true;
-               component.doneUpdatingStatus(true);
-               expect(component.changeStatusDialogOpen).toBeFalse();
+               expect(openSpy).not.toHaveBeenCalled();
+               component.isNew = false;
+               component.changeStatus(new MouseEvent("click"));
+               expect(openSpy).toHaveBeenCalled();
        });
 });
diff --git 
a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.ts
 
b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.ts
index cbd99f220d..89b497adcd 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.ts
+++ 
b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.ts
@@ -12,7 +12,9 @@
 * limitations under the License.
 */
 
+import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
 import { Component, OnInit } from "@angular/core";
+import { MatDialog } from "@angular/material/dialog";
 import { ActivatedRoute, Router } from "@angular/router";
 import { faClock as hollowClock } from "@fortawesome/free-regular-svg-icons";
 import { faClock, faMinus, faPlus, faToggleOff, faToggleOn, IconDefinition } 
from "@fortawesome/free-solid-svg-icons";
@@ -29,6 +31,11 @@ import type {
 
 import { CacheGroupService, CDNService, PhysicalLocationService, 
ProfileService, TypeService } from "src/app/api";
 import { ServerService } from "src/app/api/server.service";
+import { UpdateStatusComponent } from 
"src/app/core/servers/update-status/update-status.component";
+import {
+       DecisionDialogComponent,
+       DecisionDialogData
+} from "src/app/shared/dialogs/decision-dialog/decision-dialog.component";
 import { NavigationService } from 
"src/app/shared/navigation/navigation.service";
 import { IP, IP_WITH_CIDR, AutocompleteValue } from "src/app/utils";
 
@@ -58,24 +65,6 @@ export class ServerDetailsComponent implements OnInit {
         * A Regular Expression that matches valid IP addresses.
         */
        public validGatewayPattern = IP;
-       /**
-        * Controls whether or not the "change status" dialog is open
-        */
-       public changeStatusDialogOpen = false;
-
-       /**
-        * Tracks whether ILO details should be hidden.
-        */
-       public hideILO = false;
-       /**
-        * Tracks whether management interface details should be hidden.
-        */
-       public hideManagement = false;
-       /**
-        * Tracks whether network interface details should be hidden.
-        */
-       public hideInterfaces = false;
-
        /**
         * Icon for adding to a collection.
         */
@@ -144,7 +133,8 @@ export class ServerDetailsComponent implements OnInit {
                private readonly profileService: ProfileService,
                private readonly typeService: TypeService,
                private readonly physlocService: PhysicalLocationService,
-               private readonly navSvc: NavigationService
+               private readonly navSvc: NavigationService,
+               private readonly dialog: MatDialog
        ) {
        }
 
@@ -152,7 +142,6 @@ export class ServerDetailsComponent implements OnInit {
         * Initializes the controller based on route query parameters.
         */
        public ngOnInit(): void {
-
                const handleErr = (obj: string): (e: unknown) => void =>
                        (e: unknown): void => {
                                console.error(`Failed to get ${obj}:`, e);
@@ -202,7 +191,7 @@ export class ServerDetailsComponent implements OnInit {
                        this.serverService.getServers(Number(ID)).then(
                                s => {
                                        this.server = s;
-                                       this.navSvc.headerTitle.next(`Server 
#${this.server.id}`);
+                                       this.updateTitlebar();
                                }
                        ).catch(
                                e => {
@@ -210,18 +199,67 @@ export class ServerDetailsComponent implements OnInit {
                                }
                        );
                } else {
-                       this.server.interfaces = [{
-                               ipAddresses: [{
-                                       address: "",
-                                       gateway: null,
-                                       serviceAddress: true
+                       this.server = {
+                               cachegroup: "",
+                               cachegroupId: 0,
+                               cdnId: 0,
+                               cdnName: "",
+                               domainName: "",
+                               guid: null,
+                               hostName: "",
+                               httpsPort: null,
+                               id: 0,
+                               iloIpAddress: null,
+                               iloIpGateway: null,
+                               iloIpNetmask: null,
+                               iloPassword: null,
+                               iloUsername: null,
+                               interfaces: [{
+                                       ipAddresses: [{
+                                               address: "",
+                                               gateway: null,
+                                               serviceAddress: true
+                                       }],
+                                       maxBandwidth: null,
+                                       monitor: true,
+                                       mtu: null,
+                                       name: "",
                                }],
-                               maxBandwidth: null,
-                               monitor: false,
-                               mtu: null,
-                               name: "",
-                       }];
+                               lastUpdated: new Date(),
+                               mgmtIpAddress: null,
+                               mgmtIpGateway: null,
+                               mgmtIpNetmask: null,
+                               offlineReason: null,
+                               physLocation: "",
+                               physLocationId: 0,
+                               profileNames: [],
+                               rack: null,
+                               revalPending: false,
+                               routerHostName: null,
+                               routerPortName: null,
+                               status: "",
+                               statusId: 0,
+                               statusLastUpdated: null,
+                               tcpPort: null,
+                               type: "",
+                               typeId: 0,
+                               updPending: false,
+                               xmppId: ""
+                       };
+                       this.updateTitlebar();
+               }
+       }
+
+       /**
+        * Updates the headerTitle based on current server state.
+        *
+        * @private
+        */
+       private updateTitlebar(): void {
+               if (this.isNew) {
                        this.navSvc.headerTitle.next("New Server");
+               } else {
+                       this.navSvc.headerTitle.next(`Server: 
${this.server.hostName}`);
                }
        }
 
@@ -247,7 +285,78 @@ export class ServerDetailsComponent implements OnInit {
                                        console.error("failed to create 
server:", err);
                                }
                        );
+               } else {
+                       this.serverService.updateServer(this.server).then(
+                               responseServer => {
+                                       this.server = responseServer;
+                                       this.updateTitlebar();
+                               },
+                               err => {
+                                       console.error(`failed to update server: 
${err}`);
+                               }
+                       );
+               }
+       }
+       /**
+        * Deletes the Server.
+        */
+       public delete(): void {
+               if (this.isNew) {
+                       console.error("Unable to delete new Cache Group");
+                       return;
                }
+               const ref = this.dialog.open<DecisionDialogComponent, 
DecisionDialogData, boolean>(
+                       DecisionDialogComponent,
+                       {
+                               data: {
+                                       message: `Are you sure you want to 
delete Server ${this.server.hostName} (#${this.server.id})?`,
+                                       title: "Confirm Delete"
+                               }
+                       }
+               );
+               ref.afterClosed().subscribe(result => {
+                       if (result) {
+                               this.serverService.deleteServer(this.server);
+                               this.router.navigate(["core/servers"]);
+                       }
+               });
+       }
+
+       /**
+        * Handles when a profile list item is 'dropped'
+        *
+        * @param $event The Drop event that is emitted.
+        */
+       public drop($event: CdkDragDrop<string[]>): void {
+               moveItemInArray(this.server.profileNames, $event.previousIndex, 
$event.currentIndex);
+       }
+
+       /**
+        * Queues updates for the server
+        */
+       public async queue(): Promise<void> {
+               this.serverService.queueUpdates(this.server).then(result => {
+                       if(result.action === "queue") {
+                               this.server.updPending = true;
+                       }
+               },
+               err => {
+                       console.error(`failed to queue updates: ${err}`);
+               });
+       }
+
+       /**
+        * Dequeues updates for the server
+        */
+       public async dequeue(): Promise<void> {
+               this.serverService.clearUpdates(this.server).then(result => {
+                       if(result.action === "dequeue") {
+                               this.server.updPending = false;
+                       }
+               },
+               err => {
+                       console.error(`failed to dequeue updates: ${err}`);
+               });
        }
 
        /**
@@ -268,12 +377,34 @@ export class ServerDetailsComponent implements OnInit {
                this.server.interfaces.push(newInf);
        }
 
+       /**
+        * Returns a user-friendly name for an interface.
+        *
+        * @param inf The Interface to get the name from
+        * @returns Friendly interface name
+        */
+       public getInterfaceName(inf: Interface): string {
+               return inf.name === "" ? "<un-named>" : inf.name;
+       }
+
+       /**
+        * Finds the ID of a given profile name
+        *
+        * @param profileName The profileName to find the id of.
+        * @returns Profile id
+        */
+       public profileNameToId(profileName: string): number {
+               return (this.profiles.find(p => p.name === profileName) ?? {id: 
-1}).id;
+       }
+
        /**
         * Adds a new IP address to the server.
         *
+        * @param event The triggering DOM event; its propagation is stopped.
         * @param inf The specific network interface to which to add the new IP 
address.
         */
-       public addIP(inf: Interface): void {
+       public addIP(event: MouseEvent, inf: Interface): void {
+               event.stopPropagation();
                inf.ipAddresses.push({
                        address: "",
                        gateway: null,
@@ -284,19 +415,23 @@ export class ServerDetailsComponent implements OnInit {
        /**
         * Removes an IP address from the server.
         *
+        * @param event The triggering DOM event; its propagation is stopped.
         * @param inf The specific network interface from which to remove an IP 
address.
         * @param ip The index in the `ipAddresses` of `inf` to delete.
         */
-       public deleteIP(inf: Interface, ip: number): void {
+       public deleteIP(event: MouseEvent, inf: Interface, ip: number): void {
+               event.stopPropagation();
                inf.ipAddresses.splice(ip, 1);
        }
 
        /**
         * Removes a network interface from the server.
         *
+        * @param e The triggering DOM event; its propagation is stopped.
         * @param inf The index of the interface to remove.
         */
-       public deleteInterface(inf: number): void {
+       public deleteInterface(e: MouseEvent, inf: number): void {
+               e.stopPropagation();
                this.server.interfaces.splice(inf, 1);
        }
 
@@ -323,29 +458,19 @@ export class ServerDetailsComponent implements OnInit {
                if (this.isNew) {
                        throw new Error("cannot update the status of a server 
that doesn't exist yet");
                }
-               this.changeStatusDialogOpen = true;
-       }
-
-       /**
-        * Handles the completion of a server update, closing the dialog and 
updating the view if necessary.
-        *
-        * @param reload Whether or not the server was actually changed (and 
thus needs to be reloaded)
-        */
-       public doneUpdatingStatus(reload: boolean): void {
-               this.changeStatusDialogOpen = false;
-               if (this.isNew || !this.server.id) {
-                       console.error("done fired on server with no ID");
-                       return;
-               }
-               if (reload) {
-                       this.serverService.getServers(this.server.id).then(
-                               s => this.server = s
-                       ).catch(
-                               e => {
-                                       console.error("Failed to reload 
servers:", e);
-                               }
-                       );
-               }
+               const ref = this.dialog.open(UpdateStatusComponent, {
+                       data: [this.server]
+               });
+               ref.afterClosed().subscribe(res => {
+                       if (res) {
+                               
this.serverService.getServers(this.server.id).then(
+                                       s => this.server = s
+                               ).catch(
+                                       err => {
+                                               console.error("Failed to reload 
servers:", err);
+                                       }
+                               );
+                       }
+               });
        }
-
 }
diff --git 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
index cc133b3273..586477ca2c 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
@@ -11,18 +11,21 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<main>
-       <mat-card appearance="outlined" class="table-page-content">
-               <div class="search-container">
-                       <input type="search" name="fuzzControl" 
aria-label="Fuzzy Search Servers" inputmode="search" role="search" 
accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" 
(input)="updateURL()"/>
-               </div>
-               <tp-generic-table
-                       [data]="servers | async"
-                       [cols]="columnDefs"
-                       [fuzzySearch]="fuzzySubject"
-                       context="servers"
-                       [contextMenuItems]="contextMenuItems"
-                       (contextMenuAction)="handleContextMenu($event)">
-               </tp-generic-table>
-       </mat-card>
-</main>
+<mat-card appearance="outlined" class="page-content">
+       <div class="search-container">
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Servers" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()"/>
+       </div>
+       <tp-generic-table
+               [data]="servers | async"
+               [doubleClickLink]="doubleClickLink"
+               [cols]="columnDefs"
+               [fuzzySearch]="fuzzySubject"
+               context="servers"
+               [moreMenuButtons]="moreMenuButtons"
+               (moreMenuButtonAction)="handleMoreMenu($event)"
+               [contextMenuItems]="contextMenuItems"
+               (contextMenuAction)="handleContextMenu($event)">
+       </tp-generic-table>
+</mat-card>
+
+<a class="page-fab" mat-fab title="Create a new Server" 
*ngIf="auth.hasPermission('SERVER:CREATE')" 
routerLink="new"><mat-icon>add</mat-icon></a>
diff --git 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.spec.ts
index 055c3ac191..a813c184e2 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.spec.ts
+++ 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.spec.ts
@@ -82,9 +82,7 @@ const defaultServer: ResponseServer = {
        offlineReason: null,
        physLocation: "",
        physLocationId: 1,
-       profile: "",
-       profileDesc: "",
-       profileId: 1,
+       profileNames: ["GLOBAL"],
        rack: null,
        revalPending: false,
        routerHostName: null,
diff --git 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.ts
 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.ts
index 72eb466a4d..d1bea8b259 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.ts
+++ 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.ts
@@ -13,16 +13,25 @@
 */
 
 import { Component , type OnInit} from "@angular/core";
-import { UntypedFormControl } from "@angular/forms";
+import { FormControl } from "@angular/forms";
 import { MatDialog } from "@angular/material/dialog";
 import { ActivatedRoute } from "@angular/router";
 import type { ITooltipParams } from "ag-grid-community";
 import { BehaviorSubject } from "rxjs";
-import { type ResponseServer, serviceAddresses } from "trafficops-types";
+import { ResponseCDN, type ResponseServer, serviceAddresses } from 
"trafficops-types";
 
-import { ServerService } from "src/app/api";
+import { CDNService, ServerService } from "src/app/api";
 import { UpdateStatusComponent } from 
"src/app/core/servers/update-status/update-status.component";
-import type { ContextMenuActionEvent, ContextMenuItem } from 
"src/app/shared/generic-table/generic-table.component";
+import { CurrentUserService } from 
"src/app/shared/current-user/current-user.service";
+import {
+       CollectionChoiceDialogComponent, CollectionChoiceDialogData
+} from 
"src/app/shared/dialogs/collection-choice-dialog/collection-choice-dialog.component";
+import type {
+       ContextMenuActionEvent,
+       ContextMenuItem,
+       DoubleClickLink,
+       TableTitleButton
+} from "src/app/shared/generic-table/generic-table.component";
 import { NavigationService } from 
"src/app/shared/navigation/navigation.service";
 
 /**
@@ -76,7 +85,9 @@ export class ServersTableComponent implements OnInit {
 
        /** All of the servers which should appear in the table. */
        public servers: Promise<Array<AugmentedServer>> | null = null;
-       // public servers: Array<Server>;
+
+       /** All of the CDNs (on which a user might (de/)queue updates). */
+       public readonly cdns: Promise<Array<ResponseCDN>>;
 
        /** Definitions of the table's columns according to the ag-grid API */
        public columnDefs = [
@@ -278,6 +289,23 @@ export class ServersTableComponent implements OnInit {
                }
        ];
 
+       /** Definitions for the more menu buttons */
+       public moreMenuButtons: Array<TableTitleButton> = [
+               {
+                       action: "queue",
+                       text: "Queue Server Updates"
+               },
+               {
+                       action: "dequeue",
+                       text: "Clear Server Updates"
+               }
+       ];
+
+       /** Defines what the table should do when a row is double-clicked. */
+       public doubleClickLink: DoubleClickLink<AugmentedServer> = {
+               href: (row: AugmentedServer): string => 
`/core/servers/${row.id}`
+       };
+
        /** Definitions for the context menu items (which act on augmented 
server data). */
        public contextMenuItems: Array<ContextMenuItem<AugmentedServer>> = [
                {
@@ -320,23 +348,17 @@ export class ServersTableComponent implements OnInit {
        public fuzzySubject: BehaviorSubject<string>;
 
        /** Form controller for the user search input. */
-       public fuzzControl: UntypedFormControl = new UntypedFormControl("");
+       public fuzzControl: FormControl = new FormControl("");
 
-       /**
-        * Constructs the component with its required injections.
-        *
-        * @param api The Servers API which is used to provide row data.
-        * @param route A reference to the route of this view which is used to 
set the fuzzy search box text from the 'search' query parameter.
-        * @param router Angular router
-        * @param navSvc Manages the header
-        * @param dialog Dialog manager
-        */
        constructor(private readonly api: ServerService,
+               public readonly auth: CurrentUserService,
                private readonly route: ActivatedRoute,
                private readonly navSvc: NavigationService,
+               private readonly cdn: CDNService,
                private readonly dialog: MatDialog) {
                this.fuzzySubject = new BehaviorSubject<string>("");
                this.navSvc.headerTitle.next("Servers");
+               this.cdns = this.cdn.getCDNs();
        }
 
        /** Initializes table data, loading it from Traffic Ops. */
@@ -360,6 +382,38 @@ export class ServersTableComponent implements OnInit {
                this.fuzzySubject.next(this.fuzzControl.value);
        }
 
+       /**
+        * Handles user selection of a more menu action button.
+        *
+        * @param action The emitted more menu button action event.
+        */
+       public async handleMoreMenu(action: string): Promise<void> {
+               const data: CollectionChoiceDialogData<number> = {
+                       collection: (await this.cdns).map(cdn => ({label: 
cdn.name, value: cdn.id})),
+                       label: "Please Select a CDN",
+                       message: "",
+                       title: "Queue Server Updates"
+               };
+               switch(action) {
+                       case "dequeue":
+                               data.title = "Clear Server Updates";
+                       case "queue":
+                               const ref = 
this.dialog.open<CollectionChoiceDialogComponent, 
CollectionChoiceDialogData<number>, number | false>(
+                                       CollectionChoiceDialogComponent,
+                                       {data}
+                               );
+                               const result = await 
ref.afterClosed().toPromise();
+                               if (typeof(result) === "number") {
+                                       if (data.title.indexOf("Clear") > -1) {
+                                               await 
this.cdn.dequeueServerUpdates(result);
+                                       } else {
+                                               await 
this.cdn.queueServerUpdates(result);
+                                       }
+                               }
+                               break;
+               }
+       }
+
        /**
         * Handles user selection of a context menu action item.
         *
diff --git 
a/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.html
 
b/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.html
index 0677d96f39..1b9d913f23 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.html
+++ 
b/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.html
@@ -16,9 +16,9 @@ limitations under the License.
        <div mat-dialog-content>
                <mat-form-field appearance="fill" *ngIf="true">
                        <mat-label>New Status</mat-label>
-                       <select name="status" [(ngModel)]="status" required 
matNativeControl>
-                               <option *ngFor="let status of statuses" 
[value]="status" [disabled]="status.id === 
currentStatus">{{status.name}}</option>
-                       </select>
+                       <mat-select name="status" [(ngModel)]="status" required>
+                               <mat-option *ngFor="let s of statuses" 
[value]="s" [disabled]="s.id === currentStatus">{{s.name}}</mat-option>
+                       </mat-select>
                </mat-form-field>
                <mat-form-field *ngIf="isOffline" appearance="fill">
                        <mat-label>Offline Reason</mat-label>
diff --git 
a/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.spec.ts
index 4fe28be8f0..5472db66ff 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.spec.ts
+++ 
b/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.spec.ts
@@ -22,7 +22,7 @@ import { APITestingModule } from "src/app/api/testing";
 
 import { UpdateStatusComponent } from "./update-status.component";
 
-const defaultServer = {
+const defaultServer: ResponseServer = {
        cachegroup: "",
        cachegroupId: 1,
        cdnId: 1,
@@ -59,9 +59,7 @@ const defaultServer = {
        offlineReason: null,
        physLocation: "",
        physLocationId: 1,
-       profile: "",
-       profileDesc: "",
-       profileId: 1,
+       profileNames: ["GLOBAL"],
        rack: null,
        revalPending: false,
        routerHostName: null,
diff --git 
a/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.ts
 
b/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.ts
index f3aff528bb..4f8d0627b7 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.ts
+++ 
b/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.ts
@@ -40,7 +40,8 @@ export class UpdateStatusComponent implements OnInit {
 
        /** Tells whether the user's selected status is considered "OFFLINE". */
        public get isOffline(): boolean {
-               return this.status !== null && this.status !== undefined && 
this.status.name !== "ONLINE" && this.status.name !== "REPORTED";
+               return this.status !== null && this.status !== undefined &&
+                       this.status.name !== "ONLINE" && this.status.name !== 
"REPORTED";
        }
 
        /** An appropriate title for the server or collection of servers being 
updated. */
@@ -100,7 +101,7 @@ export class UpdateStatusComponent implements OnInit {
                let observables;
                if (this.isOffline) {
                        observables = this.servers.map(
-                               async x=>this.api.updateStatus(x, 
this.status?.name ?? "", this.offlineReason)
+                               async x=> this.api.updateStatus(x, 
this.status?.name ?? "", this.offlineReason)
                        );
                } else {
                        observables = this.servers.map(async 
x=>this.api.updateStatus(x, this.status?.name ?? ""));
diff --git 
a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.html
 
b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.html
index 20dbf15054..5309d0427a 100644
--- 
a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.html
+++ 
b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.html
@@ -16,19 +16,31 @@ limitations under the License.
        <button mat-flat-button type="button" title="de-select all rows" 
(click)="selectAll(true)">De-Select All</button>
        <button mat-flat-button type="button" title="clear all active filters" 
(click)="clearFilters()"><mat-icon>filter_list</mat-icon> Clear</button>
        <button mat-flat-button type="button"*ngFor="let btn of 
tableTitleButtons" 
(click)="emitTitleButtonAction(btn.action)">{{btn.text}}</button>
-       <button mat-flat-button type="button" (click)="download()" title="save 
as CSV"><fa-icon [icon]="downloadIcon"></fa-icon></button>
-       <button mat-flat-button type="button" 
(click)="gridAPI.sizeColumnsToFit()">Resize</button>
        <div class="toggle-columns" role="group" title="Select Table Columns">
-               <button type="button" mat-flat-button 
[matMenuTriggerFor]="menu">
+               <button type="button" mat-flat-button 
[matMenuTriggerFor]="menu" #trigger="matMenuTrigger">
                        <fa-icon [icon]="columnsIcon"></fa-icon>&nbsp;
-                       <fa-icon [icon]="caretIcon" class="caret" 
[ngClass]="{'rotate': showMenu}"></fa-icon>
+                       <fa-icon [icon]="caretIcon" class="caret" 
[ngClass]="{'rotate': trigger.menuOpen}"></fa-icon>
                </button>
                <mat-menu #menu="matMenu">
-                       <button type="button" mat-menu-item *ngFor="let c of 
columns" (click)="toggleVisibility($event, c.getColId())">
-                               <mat-checkbox [checked]="c.isVisible()" 
(click)="$event.preventDefault()" [name]="c.getColDef().headerName">
-                                       {{c.getColDef().headerName}}
-                               </mat-checkbox>
-                       </button>
+                       <div class="column-menu">
+                               <button type="button" mat-menu-item *ngFor="let 
c of columns" (click)="toggleVisibility($event, c.getColId())">
+                                       <mat-checkbox [checked]="c.isVisible()" 
[name]="c.getColDef().headerName">
+                                               {{c.getColDef().headerName}}
+                                       </mat-checkbox>
+                               </button>
+                       </div>
+               </mat-menu>
+       </div>
+       <div class="toggle-columns" role="group" title="Extra Table Actions">
+               <button type="button" mat-flat-button 
[matMenuTriggerFor]="extraMenu" #extraTrigger="matMenuTrigger">
+                       More
+                       <fa-icon [icon]="caretIcon" [ngClass]="{'rotate': 
extraTrigger.menuOpen}" class="caret"></fa-icon>
+               </button>
+               <mat-menu #extraMenu="matMenu">
+                       <button mat-menu-item type="button" *ngFor="let btn of 
moreMenuButtons" 
(click)="emitMoreButtonAction(btn.action)">{{btn.text}}</button>
+                       <mat-divider></mat-divider>
+                       <button mat-menu-item type="button" 
(click)="download()" title="save as CSV">Export Grid</button>
+                       <button mat-menu-item type="button" 
(click)="gridAPI.sizeColumnsToFit()">Resize</button>
                </mat-menu>
        </div>
 </div>
diff --git 
a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.scss
 
b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.scss
index b8d6f0ad52..b59d0c7e3e 100644
--- 
a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.scss
+++ 
b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.scss
@@ -13,13 +13,13 @@
 */
 
 ag-grid-angular {
-       width: 80vw;
+       width: 100%;
        margin: auto;
        height: 85vh;
 }
 
 .extra-actions {
-       width: 80vw;
+       width: 100%;
        display: flex;
        margin: auto;
 
@@ -44,8 +44,11 @@ ag-grid-angular {
        }
 }
 
+.column-menu {
+       max-height: 40vh;
+}
+
 .toggle-columns {
-       // margin-left: auto;
        position: relative;
 
        menu {
@@ -89,6 +92,7 @@ ag-grid-angular {
 
        button {
                fa-icon.caret {
+                       display: inline-block;
                        line-height: 30px;
                        transition: 0.5s;
 
diff --git 
a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts
 
b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts
index 219d1f89d4..3b75bdfbdc 100644
--- 
a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts
+++ 
b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts
@@ -132,6 +132,28 @@ interface ContextMenuLink<T> {
 /** ContextMenuItems represent items in a context menu. They can be links or 
arbitrary actions. */
 export type ContextMenuItem<T> = ContextMenuAction<T> | ContextMenuLink<T>;
 
+/**
+ * Specifies what happens when a row in the grid is double-clicked.
+ */
+export interface DoubleClickLink<T> {
+       /**
+        * If present, this method will be called to determine if the double 
click should be
+        * ignored.
+        *
+        * @param data The selected data which can be used to make the
+        * determination. This will be a single item if a single item is 
selected,
+        * or an array if multiple are selected.
+        * @param api A reference to the Grid's API - which must be checked for
+        * initialization, unfortunately.
+        */
+       disabled?: (selection: T | Array<T>) => boolean;
+       /**
+        * href is inserted literally as the 'href' property of an anchor. 
Which means that if it's not relative it will be mangled for security
+        * reasons.
+        */
+       href: string | ((selectedRow: T) => string);
+}
+
 /** ContextMenuActionEvent is emitted by the GenericTableComponent when an 
action in its context menu was clicked. */
 export interface ContextMenuActionEvent<T> {
        /** action is the 'action' property of the clicked action. */
@@ -278,7 +300,6 @@ export function setUpQueryParamFilter<T>(params: ParamMap, 
columns: ColDef<T>[],
                                break;
                }
                filter.setModel(filterModel);
-               // filter.applyModel();
        }
        api.onFilterChanged();
 }
@@ -305,10 +326,16 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
        @Input() public contextMenuItems: readonly 
ContextMenuItem<Readonly<T>>[] = [];
        /** Optionally a set of additional table title buttons. */
        @Input() public tableTitleButtons: Array<TableTitleButton> = [];
+       /** Optionally a set of additional more menu buttons. */
+       @Input() public moreMenuButtons: Array<TableTitleButton> = [];
+       /** Optionally a link that determines the action when double-clicking a 
grid row */
+       @Input() public doubleClickLink: DoubleClickLink<T> | undefined;
        /** Emits when context menu actions are clicked. Type safety is the 
host's responsibility! */
        @Output() public contextMenuAction = new 
EventEmitter<ContextMenuActionEvent<T>>();
        /** Emits when title button actions are clicked. Type safety is the 
host's responsibility! */
        @Output() public tableTitleButtonAction = new EventEmitter<string>();
+       /** Emits when more menu title button actions are clicked. Type safety 
is the host's responsibility! */
+       @Output() public moreMenuButtonAction = new EventEmitter<string>();
 
        public isAction = isAction;
 
@@ -325,7 +352,6 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
        public clickOutside(e: MouseEvent): void {
                e.stopPropagation();
                this.showContextMenu = false;
-               this.menuClicked = false;
        }
 
        /** This holds a reference to the table's selected data, which is 
emitted on context menu action clicks. */
@@ -353,9 +379,6 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
        /** Used to handle the case that Angular loads faster than AG-Grid (as 
it usually does) */
        private initialize = true;
 
-       /** Tracks whether the menu button has been clicked. */
-       private menuClicked = false;
-
        /** Tells whether or not to show the cell context menu. */
        public showContextMenu = false;
 
@@ -407,6 +430,19 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
                        suppressContextMenu: true,
                        tooltipShowDelay: 500
                };
+               this.gridOptions.onRowDoubleClicked = (e): void => {
+                       if (this.doubleClickLink !== undefined) {
+                               if (!this.doubleClickLink?.disabled) {
+                                       let href = "";
+                                       if (typeof (this.doubleClickLink.href) 
=== "string") {
+                                               href = 
this.doubleClickLink.href;
+                                       } else {
+                                               href = 
this.doubleClickLink.href(e.data);
+                                       }
+                                       this.router.navigate([href]);
+                               }
+                       }
+               };
        }
 
        /**
@@ -671,12 +707,6 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
        public toggleMenu(e: Event): void {
                e.stopPropagation();
                this.showContextMenu = false;
-               this.menuClicked = !this.menuClicked;
-       }
-
-       /** This tracks whether the column visibility menu is/should be open. */
-       public get showMenu(): boolean {
-               return this.menuClicked && (this.columnAPI ? true : false);
        }
 
        /** This is the styling of the table's context menu. */
@@ -709,8 +739,6 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
                        return;
                }
 
-               this.menuClicked = false;
-
                if (!this.contextmenu) {
                        console.warn("element reference to 'contextmenu' still 
null after view init");
                        return;
@@ -797,6 +825,15 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
                this.tableTitleButtonAction.emit(action);
        }
 
+       /**
+        * Handles when the user clicks on a more menu title button action item 
by emitting the proper data.
+        *
+        * @param action The action that was clicked.
+        */
+       public emitMoreButtonAction(action: string): void {
+               this.moreMenuButtonAction.emit(action);
+       }
+
        /**
         * Downloads the table data as a CSV file.
         */
diff --git 
a/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.scss
 
b/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.scss
index ed43621983..80bd35ab84 100644
--- 
a/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.scss
+++ 
b/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.scss
@@ -11,10 +11,11 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-mat-toolbar {
+
+.mat-toolbar {
        display: inline-flex;
        width: 100%;
-       height: 3em;
+       height: var(--toolbar-height);
        padding: 5px;
        position: sticky;
        top: 0;
diff --git 
a/experimental/traffic-portal/src/app/shared/navigation/tp-sidebar/tp-sidebar.component.scss
 
b/experimental/traffic-portal/src/app/shared/navigation/tp-sidebar/tp-sidebar.component.scss
index d1a6b76aa7..010c4c03dc 100644
--- 
a/experimental/traffic-portal/src/app/shared/navigation/tp-sidebar/tp-sidebar.component.scss
+++ 
b/experimental/traffic-portal/src/app/shared/navigation/tp-sidebar/tp-sidebar.component.scss
@@ -11,11 +11,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-mat-sidenav-container {
-       height: calc(100% - 3.9em);
 
-       mat-sidenav {
+.mat-sidenav-container {
+       height: calc(100% - (var(--toolbar-height) + .9em));
+
+       .mat-sidenav {
                min-width: 10em;
+               max-width: var(--sidebar-max-width);
 
                .mat-icon {
                        margin-right: 0;
diff --git a/experimental/traffic-portal/src/styles.scss 
b/experimental/traffic-portal/src/styles.scss
index fdc0032522..ea5af5ebbe 100644
--- a/experimental/traffic-portal/src/styles.scss
+++ b/experimental/traffic-portal/src/styles.scss
@@ -32,15 +32,15 @@ $traffic-portal-warn: 
mat.define-palette(mat.$orange-palette, 500);
 $traffic-portal-success: mat.define-palette(mat.$teal-palette, 300);
 $traffic-portal-error: mat.define-palette(mat.$deep-orange-palette, 600);
 $traffic-portal-theme: mat.define-light-theme((
-  color: (
-    primary: $traffic-portal-primary,
-    accent: $traffic-portal-accent,
-    warn: $traffic-portal-warn,
-  )
+       color: (
+               primary: $traffic-portal-primary,
+               accent: $traffic-portal-accent,
+               warn: $traffic-portal-warn,
+       )
 ));
 
 $traffic-portal-theme: set-ag-grid($traffic-portal-theme, (
-       odd-row-background-color: map-get(mat.$gray-palette,100),
+       odd-row-background-color: map-get(mat.$gray-palette, 100),
        row-border-color: #e2e2e2,
        border-color: #e2e2e2
 ));
@@ -59,6 +59,7 @@ $traffic-portal-theme: 
add-extra-colors($traffic-portal-theme, (
        font-display: swap;
        src: url(./assets/Roboto.300.ttf) format('truetype');
 }
+
 @font-face {
        font-family: 'Roboto';
        font-style: normal;
@@ -66,6 +67,7 @@ $traffic-portal-theme: 
add-extra-colors($traffic-portal-theme, (
        font-display: swap;
        src: url(./assets/Roboto.400.ttf) format('truetype');
 }
+
 @font-face {
        font-family: 'Roboto';
        font-style: normal;
@@ -100,7 +102,98 @@ body {
        overflow-x: hidden;
 }
 
-.mat-mdc-button-base {
+:root {
+       --toolbar-height: 3em;
+       --sidebar-max-width: 25vw;
+       --content-padding: 8vw;
+}
+
+.multi-line-tooltip {
+       white-space: pre-line;
+}
+
+.mat-mdc-hint > a {
+       text-decoration: none;
+}
+
+.page-content {
+       min-width: 640px;
+       width: calc(100% - var(--content-padding));
+       max-width: 1080px;
+       margin: 1em auto;
+
+       & > div.search-container {
+               width: 50%;
+               margin: auto;
+               padding-right: 10px;
+               position: sticky;
+               top: 0;
+               z-index: 2;
+
+               input[type="search"] {
+                       width: 100%;
+                       margin: 0 0 15px;
+               }
+
+               @media(max-width: 1230px) {
+                       & {
+                               width: 75%;
+                       }
+               }
+
+       }
+}
+
+.single-column {
+       .container {
+               display: grid;
+               grid-template-columns: 1fr;
+               grid-row-gap: 2em;
+               width: 100%;
+               margin: 1em auto 2em;
+       }
+}
+
+.double-column-responsive {
+       .container {
+               display: grid;
+               grid-template-columns: 1fr 1fr;
+               grid-column-gap: 15px;
+               grid-row-gap: 1em;
+               width: 100%;
+               margin: 1em auto 2em;
+
+               @media(max-width: 840px) {
+                       & {
+                               grid-template-columns: 1fr;
+                       }
+               }
+       }
+}
+
+.triple-column-responsive {
+       .container {
+               display: grid;
+               grid-template-columns: 1fr 1fr 1fr;
+               grid-column-gap: 15px;
+               grid-row-gap: 1em;
+               margin: 1em auto 2em;
+
+               @media(max-width: 1230px) {
+                       & {
+                               grid-template-columns: 1fr 1fr;
+                       }
+               }
+
+               @media(max-width: 840px) {
+                       & {
+                               grid-template-columns: 1fr;
+                       }
+               }
+       }
+}
+
+.mat-button-base {
        // cursor: pointer;
        text-transform: uppercase;
 }
@@ -119,8 +212,14 @@ h1, h2, h3, h4, h5, h6, label, legend {
        cursor: pointer;
 }
 
-html, body { height: 100%; }
-body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
+html, body {
+       height: 100%;
+}
+
+body {
+       margin: 0;
+       font-family: Roboto, "Helvetica Neue", sans-serif;
+}
 
 button {
        cursor: pointer;
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs.ts
 b/experimental/traffic-portal/src/styles/vars.scss
similarity index 60%
copy from 
experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs.ts
copy to experimental/traffic-portal/src/styles/vars.scss
index e9e3f80812..fc290cbdcd 100644
--- 
a/experimental/traffic-portal/nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs.ts
+++ b/experimental/traffic-portal/src/styles/vars.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -12,21 +12,30 @@
  * limitations under the License.
  */
 
-import {
-       EnhancedPageObject
-} from "nightwatch";
+@mixin small-icon-button {
+       .small-icon-button {
+               width: 24px;
+               height: 24px;
+               padding: 0px;
+               align-items: center;
+               justify-content: center;
+               display: inline-flex;
 
-/**
- * Define the type for our PO
- */
-export type DeliveryServiceInvalidPageObject = EnhancedPageObject<{}, typeof 
deliveryServiceInvalidPageObject.elements>;
+               & > *[role=img] {
+                       width: 16px;
+                       height: 16px;
+                       font-size: 16px;
+
+                       svg {
+                               width: 16px;
+                               height: 16px;
+                       }
+               }
 
-const deliveryServiceInvalidPageObject = {
-       elements: {
-               addButton: {
-                       selector: "button#new"
+               .mat-mdc-button-touch-target {
+                       width: 24px;
+                       height: 24px;
                }
        }
-};
+}
 
-export default deliveryServiceInvalidPageObject;

Reply via email to