Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve TP Tenant Selection #6623

Merged
merged 15 commits into from
Apr 1, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Traffic Monitor config option `distributed_polling` which enables the ability for Traffic Monitor to poll a subset of the CDN and divide into "local peer groups" and "distributed peer groups". Traffic Monitors in the same group are local peers, while Traffic Monitors in other groups are distibuted peers. Each TM group polls the same set of cachegroups and gets availability data for the other cachegroups from other TM groups. This allows each TM to be responsible for polling a subset of the CDN while still having a full view of CDN availability. In order to use this, `stat_polling` must be disabled.
- Added support for a new Traffic Ops GLOBAL profile parameter -- `tm_query_status_override` -- to override which status of Traffic Monitors to query (default: ONLINE).
- Traffic Router: Add support for `file`-protocol URLs for the `geolocation.polling.url` for the Geolocation database.
- Replaces all Traffic Portal Tenant select boxes with a novel tree select box [#6427](https://github.com/apache/trafficcontrol/issues/6427).
- Traffic Monitor: Add support for `access.log` to TM.
- Added functionality for login to provide a Bearer token and for that token to be later used for authorization.

Expand Down
1 change: 1 addition & 0 deletions traffic_portal/app/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ var trafficPortal = angular.module('trafficPortal', [
// directives
require('./common/directives/match').name,
require('./common/directives/dragAndDrop').name,
require('./common/directives/treeSelect').name,

// services
require('./common/service/application').name,
Expand Down
89 changes: 89 additions & 0 deletions traffic_portal/app/src/common/directives/_directives.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*


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.

*/

.has-error {
div.search-box {
label {
color: #555555;
background-color: #eeeeee;
border-color: #ccc;
}

input {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
border-color: #ccc;
}
}
}

div.tree-select-root {
position: relative;

input.display-field {
background-color: #fff;
}

div.tree-drop-down {
width: 100%;
background: white;
position: absolute;
z-index: 2;
box-shadow: rgba(0, 0, 0, 0.38) 0px 3px 3px;
border: 1px solid #0000001f;
font-size: 16px;

div.search-box {
width: calc(100% - 10px);
margin: 5px auto 5px;

@media (min-width: 768px) {
div.helptooltip {
left: calc(100% - 50px);
}
}
}

ul.nav {
max-height: 550px;
overflow-y: scroll;
overflow-x: hidden;

& > li.tree-row {
&:hover {
background: #eeeeee;
}

& > button {
padding: 5px 0 5px 5px;
position: relative;
appearance: none;
border: none;
background: transparent;
width: 100%;
text-align: left;



div {
float: left;
padding: 0 5px 0 5px;
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/

export interface TreeData {
name: string;
id: string;
children: TreeData[];
}
export interface RowData {
label: string;
value: string;
depth: number;
collapsed: boolean;
hidden: boolean;
children: RowData[];
}


/**
* Properties added to an Angular Scope either by directive binding or by being
* declared in the `link` function.
*/
export interface TreeSelectScopeProperties {
/** Returns true if the row data should be displayed after filtering. */
checkFilters: (row: RowData)=>boolean;
/** When collapse icon is clicked on row data. */
collapse: (row: RowData, evt: Event)=>void;
/**
* Gets the FontAwesome icon class based on if the row data has children and
* is collapsed.
*/
getClass: (row: RowData)=>string;
/** Used for form validation, will be assigned to an id attribute. */
handle: string;
initialValue: string;
/**
* Used to properly update the parent on value change, useful for
* validation.
*/
onUpdate: (output: {value: string})=>void;
searchText: string;
/** Updates the selection when clicking a dropdown option. */
select: (row: RowData)=>void;
selected: RowData | null;
shown: boolean;
/** Toggle the dropdown menu. */
toggle: ()=>void;
treeData: Array<TreeData>;
/** Non-recursed ordered list of rows to display (before filtering). */
treeRows: Array<RowData>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/

/** @typedef {import("angular").IController} Controller */
/** @typedef {import("angular").IDirectiveLinkFn} IDirectiveLinkFn */
/** @typedef {import("angular").IDocumentService} NGDocument */
/** @typedef {import("angular").IRootElementService} NGElement */
/** @typedef {import("angular").IScope} Scope */

/** @typedef {import("./TreeSelectDirective").RowData} RowData */
/** @typedef {import("./TreeSelectDirective").TreeData} TreeData */
/** @typedef {import("./TreeSelectDirective").TreeSelectScopeProperties} TreeSelectScopeProperties */

/**
*
* @param {NGDocument} $document
* @returns
*/
function TreeSelectDirective($document) {
return {
restrict: "E",
require: "^form",
templateUrl: "common/directives/treeSelect/tree.select.tpl.html",
replace: true,
scope: {
treeData: '=',
initialValue: '<',
handle: '@',
onUpdate: "&"
},
/**
* Controller logic for the Directive.
*
* @param {Scope & TreeSelectScopeProperties} scope
* @param {NGElement} element Host element
* @param {unknown} _ Host attribute bindings (unused)
* @param {Controller} ngFormController
*/
link: function(scope, element, _, ngFormController) {

/**
* Close the dropdown menu.
*/
function close() {
scope.shown = false;
}

/**
* Detects when a click is trigger outside this element. Used to close dropdown and ensure
* there is no $document event pollution.
*/
function closeSelect() {
close();
$document.off("click", closeSelect);
scope.$apply();
};

/**
* Converts a tree data node into row data recursively.
*
* @param {TreeData} row
* @param {number} depth
* @returns {RowData}
*/
function addNode(row, depth) {
/** @type {RowData} */
const node = {
label: row.name,
value: row.id,
depth: depth,
children: [],
collapsed: false,
hidden: false
};
scope.treeRows.push(node);
if(row.id === scope.initialValue || row.name === scope.initialValue) {
scope.selected = node
}
if(row.children !== null && row.children !== undefined) {
for(const child of row.children) {
if(child === undefined) continue;
node.children.push(addNode(child, depth + 1));
}
}
return node;
}

/**
* Collapses a row data and recursively hides and collapses its children.
*
* @param {RowData} row
* @param {boolean} [state]
*/
function collapseRecurse(row, state) {
if(row.children.length === 0) return;
for(const treeRow of scope.treeRows) {
if (treeRow.value === row.value) {
if(state === undefined)
treeRow.collapsed = !treeRow.collapsed;
else
treeRow.collapsed = state;
for(const treeChild of treeRow.children) {
treeChild.hidden = treeRow.collapsed;
collapseRecurse(treeChild, treeRow.collapsed);
}
}
}
}

/**
* Initializes treeRows, must be called whenever treeData changes
*/
function reInit() {
scope.treeRows = [];
scope.selected = null;
for(const data of scope.treeData) {
if (data !== undefined)
addNode(data, 0);
}
}

/**
* Returns true if the inputs letters are also present in text with the same order.
*
* @param {string} text
* @param {string} input
* @returns {boolean}
*/
function fuzzyMatch(text, input) {
if(input === "") return true;
if(text === undefined) return false;
text = text.toString().toLowerCase();
input = input.toString().toLowerCase();
let n = -1;
for(const i of input) {
const letter = input[i];
if (!~(n = text.indexOf(letter, n + 1))) return false;
}
return true;
}

/**
* Triggers onUpdate binding
*/
function update() {
scope.onUpdate({value: scope.selected?.value ?? ""});
}

scope.treeRows = [];
scope.searchText = "";
scope.shown = false;
scope.selected = null;

element.bind("click",
evt => {
if(scope.shown) {
evt.stopPropagation();
$document.on("click", closeSelect);
}
}
);

scope.toggle = () => scope.shown = !scope.shown;


scope.select = row => {
scope.selected = row;
update();

if(row.value !== this.initialValue) {
ngFormController[scope.handle].$setDirty();
}
close();
}

scope.collapse = (row, evt) => {
evt.stopPropagation();
collapseRecurse(row);
}

scope.checkFilters = testRow => {
if(testRow.hidden && scope.searchText.length === 0)
return false;
return fuzzyMatch(testRow.label, scope.searchText);
}

scope.getClass = function(row) {
if(row.collapsed && row.children.length > 0) return "fa-minus";
else if(row.children.length > 0) return "fa-plus";
else return "fa-users";
}

scope.$watch("treeData", reInit);

ngFormController.$removeControl(ngFormController[scope.handle + "searchText"]);
}
}
};

TreeSelectDirective.$inject = ['$document'];
module.exports = TreeSelectDirective;
Loading