UNPKG

agens-viewer

Version:

Simple AgensGraph web UI client that easy to run and use

315 lines (294 loc) 9.11 kB
import LayoutDagre from '@antv/g6/plugins/layout.dagre'; import '@antv/g6/build/plugin.behaviour.analysis'; import G6 from '@antv/g6'; import { API_ENDPOINTS, DEFAULT_SERVER_CONFIG, UI } from '../../contants'; import { COLORS_ARR, MaterialColors } from './materialColors'; import { getItem, setItem } from './storage'; import * as ko from 'knockout'; ko.options.deferUpdates = true; /** * Make string hash * * @param str * @returns {number} */ const strHash = (str) => { const s = `${str}`; let hash = 0; if (s.length === 0) {return hash;} for (let i = 0; i < s.length; i++) { const char = s.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; }; const str2color = (str, intensity = UI.DEFAULT_COLOR_INTENSITY, colors, colorsArr) => { const pos = Math.abs(strHash(str)) % colorsArr.length; const colorName = colorsArr[pos]; const color = (colors[colorName] || { [UI.DEFAULT_COLOR_INTENSITY]: '#999999' }); return color[intensity] || color[UI.DEFAULT_COLOR_INTENSITY]; }; const getProps = (p) => Object.keys(p || {}).filter(k => [ 'id', 'created_ts_nanos', 'created_ts_seconds', 'updated_ts_nanos', 'updated_ts_seconds', 'deleted_ts_nanos', 'deleted_ts_seconds', ].indexOf(k) === -1).sort(); const initGraph = (g, graphDomId, miniMapDomId, onNodeInfo) => { document.getElementById(graphDomId).innerHTML = ''; G6.registerNode('rect', { stroke: 'red 1px', getPath: function getPath(item) { const width = (Math.max(`${item.model.type || ''}`.length, `${item.model.id || ''}`.length) + 2) * UI.SCHEMA_FONT_SIZE / 2; const height = (2 + 1) * UI.SCHEMA_FONT_SIZE; return G6.Util.getRectPath(-width / 2, -height / 2, width, height, 2); }, }); const panesRect = document.getElementById('mountNode').getClientRects(); const height = panesRect[0].height; const width = panesRect[0].width; const graph = new G6.Graph({ modes: { default: ['panCanvas', 'wheelZoom'], }, container: graphDomId, fitView: 'cc', animate: true, height, width, plugins: [ new LayoutDagre({ rasesep: 300, edgesep: 100, ranksep: 100, rankdir: 'TB', }), ], defaultIntersectBox: 'rect', }); graph.node({ shape: 'rect', style: (model) => ({ fillOpacity: 0.45, lineWidth: 2, stroke: str2color(model.label, 500, MaterialColors, COLORS_ARR), fill: str2color(model.label, 50, MaterialColors, COLORS_ARR), }), label: (model) => { const p = getProps(model.props); return { text: `${model.type}\n${model.id}`, stroke: null, fill: '#555555', textAlign: 'center', fontSize: UI.SCHEMA_FONT_SIZE + 1, font: 'arial sans-serif', }; }, }); graph.edge({ style: { endArrow: true, strokeOpacity: 1, }, label(model) { return { fontSize: UI.SCHEMA_FONT_SIZE + 2, font: 'arial sans-serif', text: `${model.types.map(t => t.toUpperCase()).join('\n')}`, }; }, }); graph.on('node:mouseenter', ev => { onNodeInfo({ props: ev.item.model.props, id: ev.item.model.id, type: ev.item.model.type, }); }); graph.read(g); return graph; }; const addNode = (item, nodeById) => { const id = `${item.id.id}:${item.id.oid}`; const type = item.label; const props = item.props; if (typeof nodeById[id] === 'undefined') { nodeById[id] = { id, label: type, type, props, x: 0, y: 0, }; } return nodeById[id]; }; const addEdge = (item, edgeById) => { const id = `${item.id.id}:${item.id.oid}`; const type = item.label; const props = item.props; const start = `${item.start.id}:${item.start.oid}`; const end = `${item.end.id}:${item.end.oid}`; const edgeId = `${start}:${end}`; if (typeof edgeById[edgeId] === 'undefined') { const e = { id: edgeId, eIds: [id], types: [type], props, source: start, target: end, }; edgeById[edgeId] = e; return e; } else { if (edgeById[edgeId].eIds.indexOf(id) === -1) { edgeById[edgeId].eIds.push(id); } if (edgeById[edgeId].types.indexOf(type) === -1) { edgeById[edgeId].types.push(type); } } }; export const AgensViewerModel = function () { const that = this; this.pending = ko.observable(false); this.isSuccessful = ko.observable(null); this.executedQuery = ko.observable(''); this.historyRecords = ko.observableArray(getItem('av-history-records', [])); this.rows = ko.observableArray([]); this.queryErrors = ko.observableArray([]); this.systemErrors = ko.observableArray([]); this.query = ko.observable(((this.historyRecords() || []).length > 0) ? this.historyRecords()[0].query : DEFAULT_SERVER_CONFIG.DEFAULT_QUERY); this.clearConfirm = ko.observable(false); this.nodeInfoType = ko.observable(); this.nodeInfoId = ko.observable(); this.nodeInfoProps = ko.observable(); this.showClearConfirm = function () { this.clearConfirm(true); }; this.hideClearConfirm = function () { this.clearConfirm(false); }; this.clearHistory = function () { that.clearConfirm(false); const remaining = that.historyRecords().filter(r => r.marked); that.historyRecords(remaining); that.query(remaining.length > 0 ? remaining[0].query : that.query()); setItem('av-history-records', that.historyRecords()); }; const showNodeInfo = (node) => { this.nodeInfoType(node.type); this.nodeInfoId(node.id); this.nodeInfoProps(JSON.stringify(node.props, null, 2)); }; this.updateGraph = (data) => { const safeData = data || { nodes: [], edges: [] }; this.graph = initGraph(safeData, 'mountNode', 'minimap', showNodeInfo); }; this.updateHistory = () => { const records = (that.historyRecords() || []).filter( (r) => { return r.query === that.executedQuery(); }, ); if (records.length === 0) { const queryRecord = { marked: false, ts: (new Date()).getTime(), query: that.executedQuery(), isSuccessful: that.isSuccessful(), systemErrors: that.systemErrors(), queryErrors: that.queryErrors(), }; that.historyRecords([queryRecord].concat(that.historyRecords())); setItem('av-history-records', that.historyRecords()); } }; this.toggleHistoryRecordMark = async (h) => { const hr = that.historyRecords().map((r) => { if (r.query === h.query) { return Object.assign({}, r, { marked: !r.marked }); } return Object.assign({}, r); }); that.historyRecords(hr); setItem('av-history-records', that.historyRecords()); }; this.loadHistoryRecord = async function (h) { const selected = that.historyRecords().filter((r) => (r.query === h.query)); const notSelected = that.historyRecords().filter((r) => (r.query !== h.query)); that.historyRecords(selected.concat(notSelected)); that.query(selected[0].query); setItem('av-history-records', that.historyRecords()); }; this.sendQuery = async function () { this.pending(true); this.executedQuery(this.query()); localStorage.setItem('ag-query', this.executedQuery()); const query = this.query().replace(/[\n\r\t]/g, ' '); let data; try { const response = await fetch( API_ENDPOINTS.QUERY, { method: 'POST', body: query, }, ); data = await response.json(); } catch (error) { console.error(error); this.isSuccessful(false); this.queryErrors([]); this.rows([]); this.systemErrors([{ message: error.message, stack: error.stack }]); return; } this.isSuccessful(((!Array.isArray(data.errors)) || (data.errors.length === 0))); const q = this.executedQuery(); this.queryErrors((data.errors || []).map(qe => ({ message: qe.message, position: qe.position, head: q.substring(0, parseInt(qe.position || (q.length + 1)) - 1), tail: q.substring(parseInt(qe.position || (q.length + 1)) - 1, q.length), }))); this.rows(data.rows || []); this.systemErrors(null); const edgeById = {}; const nodeById = {}; (data.rows || []).forEach( r => Object.keys(r).forEach( variable => { const item = r[variable]; if (typeof item === 'object') { if (item.vertices && item.edges) { item.vertices.forEach(v => addNode(v, nodeById)); item.edges.forEach(e => addEdge(e, edgeById)); } else if (item.start && item.end) { // Edge addEdge(item, edgeById); } else if (item.label && item.id) { // Node addNode(item, nodeById); } } }, ), ); this.updateGraph({ nodes: Object.values(nodeById), edges: Object.values(edgeById), }); this.updateHistory(); }; return this; };