You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2023/05/22 21:30:16 UTC
[trafficcontrol] branch master updated: TPv2 bring Servers Detail/Table to parity with TPv1 (#7497)
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 <sh...@gmail.com>
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@test.com",
+ 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@test.com",
- 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 < 27 or IPv6 with CIDR < 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>
- <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;