From 4fe5260ee17bec0c1b01438effbc7c5511005a54 Mon Sep 17 00:00:00 2001 From: yadhap Dahal Date: Thu, 5 Sep 2024 09:27:07 -0400 Subject: [PATCH 1/2] Removed trace of real-bi-dashboarding. This is no longer relevent --- Tombolo/client-reactjs/src/App.js | 5 - .../application/Assets/AssetsTable.js | 60 +---- .../components/application/Assets/index.js | 2 - .../application/VisualizationDetails.js | 244 ------------------ .../20220507024107-create-visualizations.js | 59 ----- Tombolo/server/models/application.js | 5 - Tombolo/server/models/cluster.js | 4 - Tombolo/server/models/file.js | 5 - Tombolo/server/models/groups.js | 9 - Tombolo/server/models/visualization.js | 35 --- Tombolo/server/routes/file/read.js | 172 +----------- Tombolo/server/routes/groups/group.js | 36 +-- 12 files changed, 4 insertions(+), 632 deletions(-) delete mode 100644 Tombolo/client-reactjs/src/components/application/VisualizationDetails.js delete mode 100644 Tombolo/server/migrations/20220507024107-create-visualizations.js delete mode 100644 Tombolo/server/models/visualization.js diff --git a/Tombolo/client-reactjs/src/App.js b/Tombolo/client-reactjs/src/App.js index 2f3c9427c..ec5e41354 100644 --- a/Tombolo/client-reactjs/src/App.js +++ b/Tombolo/client-reactjs/src/App.js @@ -33,7 +33,6 @@ const FileTemplate = React.lazy(() => import('./components/application/templates const JobDetailsForm = React.lazy(() => import('./components/application/Jobs/JobDetails')); const IndexDetailsForm = React.lazy(() => import('./components/application/IndexDetails')); const QueryDetailsForm = React.lazy(() => import('./components/application/queries/QueryDetails')); -const VisualizationDetailsForm = React.lazy(() => import('./components/application/VisualizationDetails')); const ManualJobDetail = React.lazy(() => import('./components/application/Jobs/ManualJobDetail')); const Actions = React.lazy(() => import('./components/application/actions/actions')); const AddJobsForm = React.lazy(() => import('./components/application/Jobs/AddjobsForm/AddJobsForm')); @@ -375,10 +374,6 @@ class App extends React.Component { - { - console.log(cluster_id); - fetch('/api/file/read/visualization', { - method: 'post', - headers: authHeader(), - body: JSON.stringify({ - id: id, - application_id: applicationId, - email: authReducer.user.email, - editingAllowed: editingAllowed, - }), - }) - .then(function (response) { - if (response.ok && response.status == 200) { - return response.json(); - } - handleError(response); - }) - .then(function (data) { - if (data && data.success) { - fetchDataAndRenderTable(); - window.open(data.url); - } - }); - }; - const generateAssetIcon = (type) => { const icons = { Job: , @@ -391,25 +354,6 @@ function AssetsTable({ openGroup, handleEditGroup, refreshGroups }) { } /> - - {record.type === 'File' ? ( - record.visualization ? ( - }> - - - - - ) : ( - handleCreateVisualization(record.id, record.cluster_id)} - icon={}> - }> - - - - ) - ) : null} ), }, diff --git a/Tombolo/client-reactjs/src/components/application/Assets/index.js b/Tombolo/client-reactjs/src/components/application/Assets/index.js index 07fb6c715..d8e57ca13 100644 --- a/Tombolo/client-reactjs/src/components/application/Assets/index.js +++ b/Tombolo/client-reactjs/src/components/application/Assets/index.js @@ -172,7 +172,6 @@ const Assets = () => { File: () => goTo('file'), Index: () => goTo('index'), Query: () => goTo('query'), - 'RealBI Dashboard': () => goTo('visualizations'), 'File Template': () => goTo('fileTemplate'), Group: () => openNewGroupDialog({ edit: false, groupId: '' }), 'Edit-Group': () => openNewGroupDialog({ edit: true, groupId: '' }), @@ -301,7 +300,6 @@ const Assets = () => { { key: 'Job', icon: , label: 'Job' }, { key: 'Query', icon: , label: 'Query' }, { key: 'Index', icon: , label: 'Index' }, - { key: 'RealBI Dashboard', icon: , label: 'RealBI Dashboard' }, ]; //Generate PDF & printing task complete function diff --git a/Tombolo/client-reactjs/src/components/application/VisualizationDetails.js b/Tombolo/client-reactjs/src/components/application/VisualizationDetails.js deleted file mode 100644 index 73c3c78c5..000000000 --- a/Tombolo/client-reactjs/src/components/application/VisualizationDetails.js +++ /dev/null @@ -1,244 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { hasEditPermission } from '../common/AuthUtil.js'; -import ReactMarkdown from 'react-markdown'; -import MonacoEditor from '../common/MonacoEditor.js'; -import { Row, Col, Button, Form, Input, Select, Tabs, Spin, AutoComplete, message, Space } from 'antd'; -import { authHeader, handleError } from '../common/AuthHeader.js'; -import Text, { i18n } from '../common/Text.jsx'; - -import { debounce } from 'lodash'; -import { useHistory } from 'react-router'; - -const TabPane = Tabs.TabPane; -const Option = Select.Option; - -const formItemLayout = { - labelCol: { span: 2 }, - wrapperCol: { span: 8 }, -}; - -const initSelectedFile = { - id: '', - url: '', - name: '', - title: '', - cluster_id: '', - description: '', -}; - -function VisualizationDetails() { - const [authReducer, assetReducer, applicationReducer] = useSelector((state) => [ - state.authenticationReducer, - state.assetReducer, - state.applicationReducer, - ]); - const [form] = Form.useForm(); - const history = useHistory(); - const [formState, setFormState] = useState({ enableEdit: true, dataAltered: false, loading: false }); - - const [search, setSearch] = useState({ loading: false, error: '', data: [] }); - - const [selectedFile, setSelectedFile] = useState({ ...initSelectedFile }); - - const editingAllowed = hasEditPermission(authReducer.user); - - const handleCancel = () => history.push('/' + applicationReducer.application.applicationId + '/assets'); - - const switchToViewOnly = () => setFormState((prev) => ({ ...prev, enableEdit: false, dataAltered: true })); - - const makeFieldsEditable = () => setFormState((prev) => ({ ...prev, enableEdit: true })); - - const onChange = () => setFormState((prev) => ({ ...prev, dataAltered: true })); - - const handleOk = async (e) => { - e.preventDefault(); - try { - await form.validateFields(); - setFormState((prev) => ({ ...prev, loading: true })); - - const options = { - headers: authHeader(), - method: 'POST', - body: JSON.stringify({ - id: selectedFile.id, - fileName: selectedFile.name, - email: authReducer.user.email, - editingAllowed: editingAllowed, - clusterId: selectedFile.cluster_id, - description: selectedFile.description, - application_id: applicationReducer.application.applicationId, - groupId: assetReducer.newAsset.groupId ? assetReducer.newAsset.groupId : '', - }), - }; - - const response = await fetch('/api/file/read/visualization', options); - if (!response.ok) handleError(response); - - const data = await response.json(); - - if (data?.success) { - history.push('/' + applicationReducer.application.applicationId + '/assets'); - } else { - throw data; - } - } catch (error) { - console.log('-error--handleOk---------------------------------------'); - console.dir({ error }, { depth: null }); - console.log('------------------------------------------'); - - message.error('Failed to get create visualization'); - } - setFormState((prev) => ({ ...prev, loading: false })); - }; - - const clearState = () => { - setSearch((prev) => ({ ...prev, data: [] })); - setSelectedFile({ ...initSelectedFile }); - form.resetFields(['name', 'fileSearchValue']); - }; - - const searchFiles = useCallback( - debounce(async (searchString) => { - if (searchString.length <= 3) return; - if (!searchString.match(/^[a-zA-Z0-9_ -]*$/)) { - return message.error('Invalid search keyword. Please remove any special characters from the keyword.'); - } - - try { - setSearch((prev) => ({ ...prev, loading: true, error: '' })); - const options = { - method: 'POST', - headers: authHeader(), - body: JSON.stringify({ - app_id: applicationReducer.application.applicationId, - keyword: searchString.trim(), - }), - }; - - const response = await fetch('/api/file/read/tomboloFileSearch', options); - if (!response.ok) handleError(response); - - const suggestions = await response.json(); - setSearch((prev) => ({ prev, loading: false, data: suggestions })); - } catch (error) { - console.log('-error-----------------------------------------'); - console.dir({ error }, { depth: null }); - console.log('------------------------------------------'); - message.error('There was an error searching the files from cluster.'); - setSearch((prev) => ({ ...prev, loading: false, error: error.message })); - } - }, 500), - [] - ); - - const onFileSelected = (fileName) => { - const file = search.data.find((file) => file.name === fileName); - setSelectedFile(file); - form.setFieldsValue({ name: file.title }); - }; - - const onChangeMD = (e) => { - if (!formState.dataAltered) onChange(); - setSelectedFile((prev) => ({ ...prev, description: e.target.value })); - }; - - const controls = ( - - {formState.enableEdit ? ( - - ) : ( - - )} - - - - ); - - return ( - - - } key="1"> - -
- {!formState.enableEdit ? null : ( - } name="fileSearchValue"> - - - searchFiles(value)} - onSelect={(value) => onFileSelected(value)} - placeholder={i18n('Search jobs')} - disabled={!editingAllowed} - notFoundContent={search.loading ? : 'Not Found'}> - {search.data.map((suggestion) => ( - - ))} - - - - - - - - )} - } - name="name" - validateTrigger={['onChange', 'onBlur']} - rules={[ - { required: formState.enableEdit, message: 'Please enter a name!' }, - { pattern: new RegExp(/^[a-zA-Z0-9:._ -]*$/), message: 'Please enter a valid name' }, - { - max: 256, - message: 'Maximum of 256 characters allowed', - }, - ]}> - - - - }> - {formState.enableEdit ? ( - - ) : ( -
- -
- )} -
-
-
-
-
-
- ); -} - -export default VisualizationDetails; diff --git a/Tombolo/server/migrations/20220507024107-create-visualizations.js b/Tombolo/server/migrations/20220507024107-create-visualizations.js deleted file mode 100644 index eb5121cda..000000000 --- a/Tombolo/server/migrations/20220507024107-create-visualizations.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; -module.exports = { - up: (queryInterface, Sequelize) => { - return queryInterface.createTable('visualizations', { - id: { - allowNull: false, - primaryKey: true, - type: Sequelize.UUID, - defaultValue: Sequelize.UUIDV4, - }, - name: Sequelize.STRING, - description: Sequelize.STRING, - type: Sequelize.STRING, - url: Sequelize.STRING, - assetId: { - type: Sequelize.UUID, - references: { - model: 'file', - key: 'id', - }, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }, - clusterId: { - type: Sequelize.UUID, - references: { - model: 'cluster', - key: 'id', - }, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }, - application_id: { - type: Sequelize.UUID, - references: { - model: 'application', - key: 'id', - }, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }, - createdAt: { - allowNull: false, - type: Sequelize.DATE, - }, - updatedAt: { - allowNull: false, - type: Sequelize.DATE, - }, - deletedAt: { - allowNull: true, - type: Sequelize.DATE, - }, - }); - }, - down: (queryInterface, Sequelize) => { - return queryInterface.dropTable('visualizations'); - }, -}; diff --git a/Tombolo/server/models/application.js b/Tombolo/server/models/application.js index 4b1c5cdc8..05fc9a718 100644 --- a/Tombolo/server/models/application.js +++ b/Tombolo/server/models/application.js @@ -25,11 +25,6 @@ module.exports = (sequelize, DataTypes) => { onDelete: "CASCADE", hooks: true, }); - /* VISUALIZATIONS */ - application.hasMany(models.visualizations, { - foreignKey: "application_id", - onDelete: "CASCADE", - }); /* GITHUB SETTINGS */ application.hasMany(models.github_repo_settings, { foreignKey: "application_id", diff --git a/Tombolo/server/models/cluster.js b/Tombolo/server/models/cluster.js index 1a22da9da..e3499e047 100644 --- a/Tombolo/server/models/cluster.js +++ b/Tombolo/server/models/cluster.js @@ -79,10 +79,6 @@ module.exports = (sequelize, DataTypes) => { cluster.hasMany(models.dataflow_cluster_credentials, { foreignKey: "cluster_id", }); - cluster.hasMany(models.visualizations, { - foreignKey: "clusterId", - onDelete: "CASCADE", - }); cluster.hasMany(models.fileTemplate, { foreignKey: "cluster_id", onDelete: "CASCADE", diff --git a/Tombolo/server/models/file.js b/Tombolo/server/models/file.js index 108c2f9bb..1b36bcb80 100644 --- a/Tombolo/server/models/file.js +++ b/Tombolo/server/models/file.js @@ -51,11 +51,6 @@ module.exports = (sequelize, DataTypes) => { otherKey: 'job_id', }); - file.hasOne(models.visualizations, { - foreignKey:'assetId', - onDelete: 'CASCADE', - }); - }; return file; }; diff --git a/Tombolo/server/models/groups.js b/Tombolo/server/models/groups.js index 887e2fae7..8f5a148ed 100644 --- a/Tombolo/server/models/groups.js +++ b/Tombolo/server/models/groups.js @@ -53,15 +53,6 @@ module.exports = (sequelize, DataTypes) => { foreignKey: 'groupId', otherKey: 'assetId' }); - groups.belongsToMany(models.visualizations, { - constraints:false, - foreignKeyConstraint:false, - through: 'assets_groups', - as: 'visualizations', - foreignKey: 'groupId', - otherKey: 'assetId' - - }); // groups.hasMany(models.groups, { // as: 'child', diff --git a/Tombolo/server/models/visualization.js b/Tombolo/server/models/visualization.js deleted file mode 100644 index 9755522fb..000000000 --- a/Tombolo/server/models/visualization.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; -module.exports = (sequelize, DataTypes) => { - const visualization = sequelize.define('visualizations', { - id: { - primaryKey: true, - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - allowNull: false, - autoIncrement: false - }, - assetId: DataTypes.UUID, - name: DataTypes.STRING, - description: DataTypes.STRING, - application_id: DataTypes.UUID, - type: DataTypes.STRING, - url: DataTypes.STRING, - clusterId: DataTypes.UUID - }, {paranoid: true, freezeTableName: true}); - visualization.associate = function(models) { - visualization.belongsToMany(models.groups, { - constraints: false, - foreignKeyConstraint:false, - through: 'assets_groups', - as: 'groups', - foreignKey: 'assetId', - otherKey: 'groupId' - }); - - visualization.belongsTo(models.file, { foreignKey: 'assetId' }); - visualization.belongsTo(models.cluster, { foreignKey: 'clusterId' }); - visualization.belongsTo(models.application, { foreignKey: 'application_id' }); - - }; - return visualization; -}; \ No newline at end of file diff --git a/Tombolo/server/routes/file/read.js b/Tombolo/server/routes/file/read.js index b75d22bf5..228149bd4 100644 --- a/Tombolo/server/routes/file/read.js +++ b/Tombolo/server/routes/file/read.js @@ -21,7 +21,6 @@ let Indexes = models.indexes; let Query = models.query; let Job = models.job; const JobFile = models.jobfile; -let Visualization = models.visualizations; var request = require("request"); const validatorUtil = require("../../utils/validator"); const { body, query, validationResult } = require("express-validator"); @@ -603,175 +602,6 @@ router.get("/dataTypes", (req, res) => { } }); -router.post( - "/visualization", - [ - body("id") - .optional({ checkFalsy: true }) - .isUUID(4) - .withMessage("Invalid file id"), - body("application_id").isUUID(4).withMessage("Invalid application id"), - body("email").isEmail().withMessage("Invalid email"), - body("fileName") - .optional({ checkFalsy: true }) - .matches(/^[a-zA-Z]{1}[a-zA-Z0-9_:.\ -]*$/) - .withMessage("Invalid name"), - body("clusterId") - .optional({ checkFalsy: true }) - .isUUID(4) - .withMessage("Invalid cluster"), - body("groupId") - .optional({ checkFalsy: true }) - .isInt() - .withMessage("Invalid groupId"), - body("editingAllowed") - .isBoolean() - .withMessage("Invalid value for editingAllowed"), - ], - async (req, res) => { - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - if (!errors.isEmpty()) { - return res.status(422).json({ success: false, errors: errors.array() }); - } - - try { - let file = null, - cluster = null, - bodyObj = {}, - authToken = ""; - if (req.body.id) { - file = await File.findOne({ where: { id: req.body.id } }); - } - - if (file && file.cluster_id) { - cluster = await Cluster.findOne({ where: { id: file.cluster_id } }); - } - - bodyObj = { - filename: file ? file.name : req.body.name, - workspaceName: "Tombolo", - dashboardName: file && file.name ? file.name : req.body.fileName, - editingAllowed: req.body.editingAllowed, - }; - - if (cluster) { - bodyObj.cluster = { - name: cluster.name, - host: cluster.thor_host, - infoPort: cluster.thor_port, - dataPort: cluster.roxie_port, - }; - } - - if ( - req.headers["authorization"] && - req.headers["authorization"].startsWith("Bearer ") - ) { - authToken = req.headers["authorization"].slice( - 7, - req.headers["authorization"].length - ); - } - - request.post( - { - url: process.env["REALBI_URL"] + "/api/v1/integration", - body: JSON.stringify(bodyObj), - headers: { - "content-type": "application/json", - Authorization: authToken, - }, - }, - async function (err, response, body) { - if (err) { - console.log("ERROR - ", err); - return res - .status(500) - .send("Error occured while creating visualization"); - } else { - var result = JSON.parse(body); - - let viz = await Visualization.create({ - name: req.body.fileName ? req.body.fileName : file.name, - application_id: req.body.application_id, - url: result.workspaceUrl, - type: req.body.type, - description: req.body.description, - assetId: file ? file.id : "", - clusterId: file ? file.cluster_id : "", - }); - if (req.body.groupId && req.body.groupId != "") { - let assetsGroups = await AssetsGroups.findOrCreate({ - where: { assetId: viz.id, groupId: req.body.groupId }, - defaults: { - assetId: viz.id, - groupId: req.body.groupId, - }, - }); - } - res.json({ success: true, url: result.workspaceUrl }); - } - } - ); - } catch (err) { - console.log(err); - return res.status(500).send("Error occured while creating visualization"); - } - } -); - -router.post( - "/deleteVisualization", - [body("id").isUUID(4).withMessage("Invalid id")], - async (req, res) => { - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - if (!errors.isEmpty()) { - return res.status(422).json({ success: false, errors: errors.array() }); - } - try { - let vizDestroyed = await Visualization.destroy({ - where: { id: req.body.id }, - }); - let assetsGroupsDestroyed = await AssetsGroups.destroy({ - where: { assetId: req.body.id }, - }); - - res.json({ success: true }); - } catch (err) { - console.log(err); - return res.status(500).send("Error occured while deleting visualization"); - } - } -); - -router.get( - "/getVisualizationDetails", - [query("id").isUUID(4).withMessage("Invalid id")], - async (req, res) => { - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - if (!errors.isEmpty()) { - return res.status(422).json({ success: false, errors: errors.array() }); - } - try { - let visualization = await Visualization.findOne({ - where: { id: req.query.id }, - attributes: ["id", "name", "url", "description", "clusterId"], - }); - res.json(visualization); - } catch (err) { - console.log(err); - return res - .status(500) - .send("Error occured while retrieving visualization details"); - } - } -); router.post( "/tomboloFileSearch", @@ -795,7 +625,7 @@ router.post( console.log(err); return res .status(500) - .send("Error occured while retrieving visualization details"); + .send("Error occured while retrieving file details"); } } ); diff --git a/Tombolo/server/routes/groups/group.js b/Tombolo/server/routes/groups/group.js index b0c16c013..d1291021e 100644 --- a/Tombolo/server/routes/groups/group.js +++ b/Tombolo/server/routes/groups/group.js @@ -11,7 +11,6 @@ let Index = models.indexes; let File = models.file; let Query = models.query; let Job = models.job; -let Visualization = models.visualizations; let AssetsGroups = models.assets_groups; let FileTemplate = models.fileTemplate; @@ -180,14 +179,11 @@ router.get('/assets', [ "id": req.query.group_id }, include: [ - {model:File, as:'files', attributes:['id', 'name', 'title', 'description', 'createdAt'], - include:[{model: Visualization}] - }, + {model:File, as:'files', attributes:['id', 'name', 'title', 'description', 'createdAt']}, {model:FileTemplate, as:'fileTemplates', attributes:['id', 'title', 'description', 'createdAt']}, {model:Job, as: 'jobs', attributes:['id', 'name', 'title', 'description', 'createdAt']}, {model:Query, as: 'queries', attributes:['id', 'name', 'title', 'description', 'createdAt']}, {model:Index, as: 'indexes', attributes:['id', 'name', 'title', 'description', 'createdAt']}, - {model:Visualization, as: 'visualizations', attributes:['id', 'name', 'description', 'url', 'createdAt']} ], order: [['name', 'ASC']] }).then(async (assets) => { @@ -199,7 +195,6 @@ router.get('/assets', [ name: file.name, title: file.title, description: file.description, - visualization: file.visualization ? file.visualization.url : null, createdAt: file.createdAt }) }) @@ -242,16 +237,6 @@ router.get('/assets', [ createdAt: query.createdAt }) }) - assets[0] && assets[0].visualizations.forEach((visualization) => { - finalAssets.push({ - type: 'RealBI Dashboard', - id: visualization.id, - name: visualization.name, - description: visualization.description, - url: visualization.url, - createdAt: visualization.createdAt - }) - }) finalAssets.sort(comparator); finalAssets = childGroups.concat(finalAssets); @@ -353,25 +338,6 @@ router.get('/assets', [ }) })) - promises.push(Visualization.findAll({ - where:{ - application_id:req.query.app_id, - [Op.and]:Sequelize.literal('not exists (select * from assets_groups where assets_groups.assetId = visualizations.id)') - } - }).then((visualizations) => { - visualizations.forEach((visualization) => { - finalAssets.push({ - type: 'RealBI Dashboard', - id: visualization.id, - name: visualization.name, - title: visualization.title, - description: visualization.description, - url: visualization.url, - createdAt: visualization.createdAt - }) - }) - })) - promises.push(Groups.findAll({where:{application_id:req.query.app_id, parent_group:{[Op.or]:[null, '']}}, order: [['name', 'ASC']]}).then((groups) => { groups.forEach((group) => { finalGroups.push({ From 5d39958382fc8ec5f04977580ca18646bc4b2380 Mon Sep 17 00:00:00 2001 From: yadhap Dahal Date: Thu, 5 Sep 2024 09:41:17 -0400 Subject: [PATCH 2/2] Removed link to open real-bi dashboard from the assets table --- .../src/components/application/Assets/AssetsTable.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Tombolo/client-reactjs/src/components/application/Assets/AssetsTable.js b/Tombolo/client-reactjs/src/components/application/Assets/AssetsTable.js index 05b46ed41..4cb1054d4 100644 --- a/Tombolo/client-reactjs/src/components/application/Assets/AssetsTable.js +++ b/Tombolo/client-reactjs/src/components/application/Assets/AssetsTable.js @@ -73,7 +73,7 @@ function AssetsTable({ openGroup, handleEditGroup, refreshGroups }) { }, [selectedAsset]); //When edit icon is clicked - const handleEdit = (id, type, action, vizUrl) => { + const handleEdit = (id, type, action) => { dispatch(assetsActions.assetSelected(id, applicationId, '')); switch (type) { @@ -92,9 +92,6 @@ function AssetsTable({ openGroup, handleEditGroup, refreshGroups }) { case 'Query': history.push('/' + applicationId + '/assets/query/' + id); break; - case 'RealBI Dashboard': - window.open(vizUrl); - break; case 'Group': if (action !== 'edit') { openGroup(id); @@ -186,7 +183,6 @@ function AssetsTable({ openGroup, handleEditGroup, refreshGroups }) { Query: , Group: , 'File Template': , - 'RealBI Dashboard': , }; return {icons[type]}; };