traffic_portal/app/src/common/modules/table/agGrid/CommonGridController.js (538 lines of code) (raw):
/*
* 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('./CommonGridController').CGC } CGC */
/**
* Given some query parameters, the columns of a table, and a hook into the
* AG-Grid API of said table, sets up filtering based on matches between the
* names of query parameters and the raw data fields of the columns.
*
* @param {URLSearchParams} params
* @param {{field?: string; filter?: string}[]} columns
* @param {GridApi} api
*/
function setUpQueryParamFilter(params, columns, api) {
for (const col of columns) {
if (!Object.prototype.hasOwnProperty.call(col, "field")) {
continue;
}
const filter = api.getFilterInstance(col.field);
if (!filter) {
continue;
}
const values = params.getAll(col.field);
if (values.length < 1) {
continue;
}
/** @type {"string" | "number" | "date"} */
let colType;
if (!Object.prototype.hasOwnProperty.call(col, "filter")) {
colType = "string";
} else if (typeof(col.filter) !== "string") {
continue;
} else {
let bail = false;
switch(col.filter) {
case "agTextColumnFilter":
colType = "string";
break;
case "agNumberColumnFilter":
colType = "number";
break;
case "agDateColumnFilter":
colType = "date";
break;
default:
bail = true;
break;
}
if (bail) {
continue;
}
}
let filterModel;
switch(colType) {
case "string":
if (values.length === 1) {
filterModel = {
filter: values[0],
type: "equals"
}
} else {
filterModel = {
operator: "OR",
condition1: {
filter: values[0],
type: "equals"
},
condition2: {
filter: values[1],
type: "equals"
}
}
}
break;
case "number":
if (values.length === 1) {
filterModel = {
filter: parseInt(values[0], 10),
type: "equals"
}
if (isNaN(filterModel.filter)) {
continue;
}
} else {
filterModel = {
operator: "OR",
condition1: {
filter: parseInt(values[0], 10),
type: "equals"
},
condition2: {
filter: parseInt(values[1], 10),
type: "equals"
}
}
if (isNaN(filterModel.condition1.filter) || isNaN(filterModel.condition2.filter)) {
continue;
}
}
break;
case "date":
const date = new Date(values[0]);
if (Number.isNaN(date.getTime())) {
continue;
}
const pad = num => String(num).padStart(2,"0");
filterModel = {
dateFrom: `${date.getUTCFullYear()}-${pad(date.getUTCMonth()+1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`,
type: "equals"
}
break;
}
filter.setModel(filterModel);
filter.applyModel();
}
}
/**
* @param {*} $scope
* @param {import("angular").IDocumentService} $document
* @param {*} $state
* @param {import("../../../models/UserModel")} userModel
* @param {import("../../../service/utils/DateUtils")} dateUtils
*/
let CommonGridController = function ($scope, $document, $state, userModel, dateUtils) {
this.entry = null;
this.quickSearch = "";
this.pageSize = 100;
this.showMenu = false;
/**
* @type {{
* bottom?: string | 0;
* left: string | 0;
* right?: string | 0;
* top: string | 0;
* }}
*/
this.menuStyle = {
left: 0,
top: 0
};
this.mouseDownSelectionText = "";
// Bound Variables
/** @type string */
this.tableTitle = "";
/** @type string */
this.tableName = "";
/** @type CGC.GridSettings */
this.options = {};
/** @type any */
this.gridOptions = {};
/** @type any[] */
this.columns = [];
/** @type string[] */
this.sensitiveColumns = [];
/** @type any[] */
this.data = [];
/** @type any[] */
this.selectedData = [];
/** @type any */
this.defaultData = {};
/** @type CGC.DropDownOption[] */
this.dropDownOptions = [];
/** @type CGC.ContextMenuOption[] */
this.contextMenuOptions = [];
/** @type CGC.TitleButton */
this.titleButton = {};
/** @type CGC.TitleBreadCrumbs */
this.breadCrumbs = [];
function HTTPSCellRenderer() {}
HTTPSCellRenderer.prototype.init = function(params) {
this.eGui = document.createElement("a");
this.eGui.href = "https://" + params.value;
this.eGui.setAttribute("class", "link");
this.eGui.setAttribute("target", "_blank");
this.eGui.textContent = params.value;
};
HTTPSCellRenderer.prototype.getGui = function() {return this.eGui;};
// browserify can't handle classes...
function SSHCellRenderer() {}
SSHCellRenderer.prototype.init = function(params) {
this.eGui = document.createElement("a");
this.eGui.href = "ssh://" + userModel.user.username + "@" + params.value;
this.eGui.setAttribute("class", "link");
this.eGui.textContent = params.value;
};
SSHCellRenderer.prototype.getGui = function() {return this.eGui;};
function CheckCellRenderer() {}
CheckCellRenderer.prototype.init = function(params) {
this.eGui = document.createElement("i");
if (params.value === null || params.value === undefined) {
return;
}
this.eGui.setAttribute("aria-hidden", "true");
this.eGui.setAttribute("title", String(params.value));
this.eGui.classList.add("fa", "fa-lg");
if (params.value) {
this.eGui.classList.add("fa-check");
} else {
this.eGui.classList.add("fa-times");
}
};
CheckCellRenderer.prototype.getGui = function() {return this.eGui;};
function UpdateCellRenderer() {}
UpdateCellRenderer.prototype.init = function(params) {
this.eGui = document.createElement("i");
this.eGui.setAttribute("aria-hidden", "true");
this.eGui.setAttribute("title", String(params.value));
this.eGui.classList.add("fa", "fa-lg");
if (params.value) {
this.eGui.classList.add("fa-clock-o");
} else {
this.eGui.classList.add("fa-check");
}
};
UpdateCellRenderer.prototype.getGui = function() {return this.eGui;};
function defaultTooltip(params) {
return params.value;
}
function dateCellFormatterRelative(params) {
return params.value ? dateUtils.getRelativeTime(params.value) : params.value;
}
function dateCellFormatterUTC(params) {
return params.value ? params.value.toUTCString() : params.value;
}
this.hasContextItems = function() {
return this.contextMenuOptions.length > 0;
};
this.hasSensitiveColumns = function() {
return this.sensitiveColumns.length > 0;
}
/**
* @param {string} colID
*/
this.isSensitive = function(colID) {
return this.sensitiveColumns.includes(colID);
}
this.sensitiveColumnsShown = false;
this.toggleSensitiveFields = function() {
if (this.sensitiveColumnsShown) {
return;
}
for (const col of this.gridOptions.columnApi.getAllColumns()) {
const id = col.getColId();
if (this.isSensitive(id)) {
this.gridOptions.columnApi.setColumnVisible(id, false);
}
}
};
this.getColumns = () => {
/** @type {{colId: string}[]} */
const cols = this.gridOptions.columnApi.getAllColumns();
if (!this.hasSensitiveColumns || this.sensitiveColumnsShown) {
return cols;
}
return cols.filter(c => !this.isSensitive(c.colId));
}
this.$onInit = () => {
const tableName = this.tableName;
if (this.defaultData !== undefined) {
this.entry = this.defaultData;
}
for(let i = 0; i < this.columns.length; ++i) {
if (this.columns[i].filter === "agDateColumnFilter") {
if (this.columns[i].relative) {
this.columns[i].tooltipValueGetter = dateCellFormatterRelative;
this.columns[i].valueFormatter = dateCellFormatterRelative;
}
else {
this.columns[i].tooltipValueGetter = dateCellFormatterUTC;
this.columns[i].valueFormatter = dateCellFormatterUTC;
}
} else if (this.columns[i].filter === 'arrayTextColumnFilter') {
this.columns[i].filter = 'agTextColumnFilter'
this.columns[i].filterParams = {
textCustomComparator: (filter, value, filterText) => {
const filterTextLowerCase = filterText.toLowerCase();
const valueLowerCase = value.toString().toLowerCase();
const profileNameValue = valueLowerCase.split(",");
switch (filter) {
case 'contains':
return valueLowerCase.indexOf(filterTextLowerCase) >= 0;
case 'notContains':
return valueLowerCase.indexOf(filterTextLowerCase) === -1;
case 'equals':
return profileNameValue.includes(filterTextLowerCase);
case 'notEqual':
return !profileNameValue.includes(filterTextLowerCase);
case 'startsWith':
return valueLowerCase.indexOf(filterTextLowerCase) === 0;
case 'endsWith':
let index = valueLowerCase.lastIndexOf(filterTextLowerCase);
return index >= 0 && index === (valueLowerCase.length - filterTextLowerCase.length);
default:
// should never happen
console.warn('invalid filter type ' + filter);
return false;
}
}
}
}
}
// clicks outside the context menu will hide it
$document.bind("click", e => {
this.showMenu = false;
e.stopPropagation();
$scope.$apply();
});
this.gridOptions = {
components: {
httpsCellRenderer: HTTPSCellRenderer,
sshCellRenderer: SSHCellRenderer,
updateCellRenderer: UpdateCellRenderer,
checkCellRenderer: CheckCellRenderer,
},
columnDefs: this.columns,
enableCellTextSelection: true,
suppressMenuHide: true,
multiSortKey: 'ctrl',
alwaysShowVerticalScroll: true,
defaultColDef: {
filter: true,
sortable: true,
resizable: true,
tooltipValueGetter: defaultTooltip
},
rowClassRules: this.options.rowClassRules,
rowData: this.data,
pagination: true,
paginationPageSize: this.pageSize,
rowBuffer: 0,
onColumnResized: () => {
/** @type {{colId: string; hide?: boolean | null}[]} */
const states = this.gridOptions.columnApi.getColumnState();
for (const state of states) {
state.hide = state.hide || this.isSensitive(state.colId);
}
localStorage.setItem(tableName + "_table_columns", JSON.stringify(states));
},
colResizeDefault: "shift",
tooltipShowDelay: 500,
allowContextMenuWithControlKey: true,
preventDefaultOnContextMenu: this.hasContextItems(),
onCellMouseDown: () => {
const selection = window.getSelection();
if (!selection) {
this.mouseDownSelectionText = "";
} else {
this.mouseDownSelectionText = selection.toString();
}
},
onCellContextMenu: params => {
if (!this.hasContextItems()){
return;
}
this.showMenu = true;
this.menuStyle.left = String(params.event.clientX) + "px";
this.menuStyle.top = String(params.event.clientY) + "px";
this.menuStyle.bottom = "unset";
this.menuStyle.right = "unset";
$scope.$apply();
const boundingRect = document.getElementById("context-menu")?.getBoundingClientRect();
if (!boundingRect) {
throw new Error("no bounding rectangle for context-menu; element possibly missing");
}
if (boundingRect.bottom > window.innerHeight){
this.menuStyle.bottom = String(window.innerHeight - params.event.clientY) + "px";
this.menuStyle.top = "unset";
}
if (boundingRect.right > window.innerWidth) {
this.menuStyle.right = String(window.innerWidth - params.event.clientX) + "px";
this.menuStyle.left = "unset";
}
this.entry = params.data;
$scope.$apply();
},
onColumnVisible: params => {
if (params.visible){
return;
}
for (let column of params.columns) {
if (column.filterActive) {
const filterModel = this.gridOptions.api.getFilterModel();
if (column.colId in filterModel) {
delete filterModel[column.colId];
this.gridOptions.api.setFilterModel(filterModel);
}
}
}
},
onRowSelected: () => {
this.selectedData = this.gridOptions.api.getSelectedRows();
$scope.$apply();
},
onSelectionChanged: () => {
this.selectedData = this.gridOptions.api.getSelectedRows();
$scope.$apply();
},
onRowClicked: params => {
if (params.event.target instanceof HTMLAnchorElement) {
return;
}
const selection = window.getSelection();
if (this.options.onRowClick !== undefined) {
if (!selection || selection.toString() === "" || selection === $scope.mouseDownSelectionText) {
this.options.onRowClick(params);
$scope.$apply();
}
}
$scope.mouseDownSelectionText = "";
},
onFirstDataRendered: () => {
if(this.options.selectRows) {
this.gridOptions.rowSelection = this.options.selectRows ? "multiple" : "";
this.gridOptions.rowMultiSelectWithClick = this.options.selectRows;
this.gridOptions.api.forEachNode(node => {
if (node.data[this.options.selectionProperty] === true) {
node.setSelected(true, false);
}
});
}
try {
const filterState = JSON.parse(localStorage.getItem(tableName + "_table_filters") ?? "{}") || {};
this.gridOptions.api.setFilterModel(filterState);
} catch (e) {
console.error("Failure to load stored filter state:", e);
}
// Set up filters from query string paramters.
const params = new URLSearchParams(globalThis.location.hash.split("?").slice(1).join("?"));
setUpQueryParamFilter(params, this.columns, this.gridOptions.api);
this.gridOptions.api.onFilterChanged();
this.gridOptions.api.addEventListener("filterChanged", () => {
localStorage.setItem(tableName + "_table_filters", JSON.stringify(this.gridOptions.api.getFilterModel()));
});
},
onGridReady: () => {
try {
// need to create the show/hide column checkboxes and bind to the current visibility
const colstates = JSON.parse(localStorage.getItem(tableName + "_table_columns") ?? "null");
if (colstates) {
if (!this.gridOptions.columnApi.setColumnState(colstates)) {
console.error("Failed to load stored column state: one or more columns not found");
}
} else {
this.gridOptions.api.sizeColumnsToFit();
}
} catch (e) {
console.error("Failure to retrieve required column info from localStorage (key=" + tableName + "_table_columns):", e);
}
try {
const sortState = JSON.parse(localStorage.getItem(tableName + "_table_sort") ?? "{}");
this.gridOptions.api.setSortModel(sortState);
} catch (e) {
console.error("Failure to load stored sort state:", e);
}
try {
this.quickSearch = localStorage.getItem(tableName + "_quick_search") ?? "";
this.gridOptions.api.setQuickFilter(this.quickSearch);
} catch (e) {
console.error("Failure to load stored quick search:", e);
}
try {
const ps = Number(localStorage.getItem(tableName + "_page_size"));
if (ps > 0) {
this.pageSize = Number(ps);
this.gridOptions.api.paginationSetPageSize(this.pageSize);
}
} catch (e) {
console.error("Failure to load stored page size:", e);
}
try {
const page = parseInt(localStorage.getItem(tableName + "_table_page") ?? "0", 10);
if (page > 0 && page <= $scope.gridOptions.api.paginationGetTotalPages()-1) {
$scope.gridOptions.api.paginationGoToPage(page);
}
} catch (e) {
console.error("Failed to load stored page number:", e);
}
this.gridOptions.api.addEventListener("sortChanged", () => {
localStorage.setItem(tableName + "_table_sort", JSON.stringify(this.gridOptions.api.getSortModel()));
});
this.gridOptions.api.addEventListener("columnMoved", () => {
/** @type {{colId: string; hide?: boolean | null}[]} */
const states = this.gridOptions.columnApi.getColumnState();
for (const state of states) {
state.hide = state.hide || this.isSensitive(state.colId);
}
localStorage.setItem(tableName + "_table_columns", JSON.stringify(this.gridOptions.columnApi.getColumnState()));
});
this.gridOptions.api.addEventListener("columnVisible", () => {
this.gridOptions.api.sizeColumnsToFit();
try {
const colStates = this.gridOptions.columnApi.getColumnState();
localStorage.setItem(tableName + "_table_columns", JSON.stringify(colStates));
} catch (e) {
console.error("Failed to store column defs to local storage:", e);
}
});
}
};
};
this.exportCSV = function() {
const params = {
allColumns: true,
fileName: this.tableName + ".csv",
};
this.gridOptions.api.exportDataAsCsv(params);
};
this.toggleVisibility = function(col) {
const visible = this.gridOptions.columnApi.getColumn(col).isVisible();
this.gridOptions.columnApi.setColumnVisible(col, !visible);
};
this.onQuickSearchChanged = function() {
this.gridOptions.api.setQuickFilter(this.quickSearch);
localStorage.setItem(this.tableName + "_quick_search", this.quickSearch);
};
this.onPageSizeChanged = function() {
const value = Number(this.pageSize);
this.gridOptions.api.paginationSetPageSize(value);
localStorage.setItem(this.tableName + "_page_size", value.toString());
};
this.clearTableFilters = () => {
// clear the quick search
this.quickSearch = '';
this.onQuickSearchChanged();
// clear any column filters
this.gridOptions.api.setFilterModel(null);
};
this.contextMenuClick = function(menu, $event) {
$event.stopPropagation();
menu.onClick(this.entry);
};
this.getHref = function(menu) {
if (menu.href !== undefined){
return menu.href;
}
return menu.getHref(this.entry);
};
this.contextIsDisabled = function(menu) {
if (menu.isDisabled !== undefined) {
return menu.isDisabled(this.entry);
}
return false;
};
this.bcGetText = function (bc) {
if(bc.text !== undefined){
return bc.text;
}
return bc.getText();
};
this.bcHasHref = function(bc) {
return bc.href !== undefined || bc.getHref !== undefined;
};
this.bcGetHref = function(bc) {
if(bc.href !== undefined) {
return bc.href;
}
return bc.getHref();
};
this.getText = function (menu) {
if (menu.text !== undefined){
return menu.text;
}
return menu.getText(this.entry);
};
this.isShown = function (menu) {
if (menu.shown === undefined){
return true;
}
return menu.shown(this.entry);
};
$scope.refresh = function() {
$state.reload(); // reloads all the resolves for the view
};
};
angular.module("trafficPortal.table").component("commonGridController", {
templateUrl: "common/modules/table/agGrid/grid.tpl.html",
controller: CommonGridController,
bindings: {
tableTitle: "@",
tableName: "@",
options: "<",
columns: "<",
data: "<",
selectedData: "=?",
dropDownOptions: "<?",
contextMenuOptions: "<?",
defaultData: "<?",
titleButton: "<?",
breadCrumbs: "<?",
sensitiveColumns: "<?"
}
});
CommonGridController.$inject = ["$scope", "$document", "$state", "userModel", "dateUtils"];
module.exports = CommonGridController;