snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
3
client/web/reporter/.browserslistrc
Normal file
3
client/web/reporter/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
16
client/web/reporter/.editorconfig
Normal file
16
client/web/reporter/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
10
client/web/reporter/.eslintrc.js
Normal file
10
client/web/reporter/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
extends: ['corteza-client'],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
requireConfigFile: false,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
}
|
||||
20
client/web/reporter/.gitignore
vendored
Normal file
20
client/web/reporter/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
/.act*
|
||||
/workflow
|
||||
39
client/web/reporter/Makefile
Normal file
39
client/web/reporter/Makefile
Normal file
@@ -0,0 +1,39 @@
|
||||
.PHONY: dep test build release upload
|
||||
|
||||
YARN_FLAGS ?= --non-interactive --no-progress --silent --emoji false
|
||||
YARN = yarn $(YARN_FLAGS)
|
||||
|
||||
REPO_NAME ?= corteza-webapp-reporter
|
||||
|
||||
BUILD_FLAVOUR ?= corteza
|
||||
BUILD_FLAGS ?= --production
|
||||
BUILD_DEST_DIR = dist
|
||||
BUILD_TIME ?= $(shell date +%FT%T%z)
|
||||
BUILD_VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||
BUILD_NAME = $(REPO_NAME)-$(BUILD_VERSION)
|
||||
|
||||
RELEASE_NAME = $(BUILD_NAME).tar.gz
|
||||
RELEASE_EXTRA_FILES ?= README.md LICENSE CONTRIBUTING.md DCO
|
||||
RELEASE_PKEY ?= .upload-rsa
|
||||
|
||||
dep:
|
||||
$(YARN) install
|
||||
|
||||
test:
|
||||
$(YARN) lint
|
||||
$(YARN) test:unit
|
||||
|
||||
build:
|
||||
export BUILD_VERSION=${BUILD_VERSION} && $(YARN) build $(BUILD_FLAGS)
|
||||
|
||||
release:
|
||||
@ cp $(RELEASE_EXTRA_FILES) $(BUILD_DEST_DIR)
|
||||
@ tar -C $(BUILD_DEST_DIR) -czf $(RELEASE_NAME) $(dir $(BUILD_DEST_DIR))
|
||||
|
||||
upload: $(RELEASE_PKEY)
|
||||
@ echo "put *.tar.gz" | sftp -q -o "StrictHostKeyChecking no" -i $(RELEASE_PKEY) $(RELEASE_SFTP_URI)
|
||||
@ rm -f $(RELEASE_PKEY)
|
||||
|
||||
$(RELEASE_PKEY):
|
||||
@ echo $(RELEASE_SFTP_KEY) | base64 -d > $(RELEASE_PKEY)
|
||||
@ chmod 0400 $@
|
||||
5
client/web/reporter/babel.config.js
Normal file
5
client/web/reporter/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@vue/app', { useBuiltIns: 'entry' }],
|
||||
],
|
||||
}
|
||||
36
client/web/reporter/jest.config.js
Normal file
36
client/web/reporter/jest.config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: '@vue/cli-plugin-unit-jest',
|
||||
testMatch: [
|
||||
'**/tests/unit/**/*.spec.js',
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^corteza-webapp-reporter/(.*)$': '<rootDir>/$1',
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'jsx',
|
||||
'json',
|
||||
'vue',
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.vue$': '@vue/vue2-jest',
|
||||
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(chai|sinon)/)',
|
||||
],
|
||||
snapshotSerializers: [
|
||||
'jest-serializer-vue',
|
||||
],
|
||||
testEnvironment: 'jsdom',
|
||||
testURL: 'http://localhost/',
|
||||
watchPlugins: [
|
||||
'jest-watch-typeahead/filename',
|
||||
'jest-watch-typeahead/testname',
|
||||
],
|
||||
setupFiles: [
|
||||
'<rootDir>/tests/setup.js',
|
||||
],
|
||||
}
|
||||
87
client/web/reporter/package.json
Normal file
87
client/web/reporter/package.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "corteza-webapp-reporter",
|
||||
"description": "Corteza Reporter WebApp",
|
||||
"version": "2024.9.10",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev-env": "vue-cli-service serve src/dev-env.js",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"lint": "vue-cli-service lint --fix",
|
||||
"cdeps": "yarn link @cortezaproject/corteza-js @cortezaproject/corteza-vue",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test": "yarn test:unit"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "yarn lint"
|
||||
},
|
||||
"stylelint": {
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"color-hex-length": null,
|
||||
"no-empty-source": null
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"postcss": "^8.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cortezaproject/corteza-js": "^2024.9.10",
|
||||
"@cortezaproject/corteza-vue": "2024.9.10",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.10",
|
||||
"@panter/vue-i18next": "^0.15.0",
|
||||
"bootstrap-vue": "2.23.1",
|
||||
"echarts": "5.6.0",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"portal-vue": "^2.1.7",
|
||||
"vue": "2.7.16",
|
||||
"vue-echarts": "7.0.3",
|
||||
"vue-grid-layout": "^2.3.12",
|
||||
"vue-native-websocket": "^2.0.15",
|
||||
"vue-router": "3.1.5",
|
||||
"vue-split-panel": "^1.0.4",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.7",
|
||||
"@babel/eslint-parser": "^7.26.5",
|
||||
"@vue/cli-plugin-babel": "5.0.8",
|
||||
"@vue/cli-plugin-eslint": "5.0.8",
|
||||
"@vue/cli-plugin-unit-jest": "^5.0.8",
|
||||
"@vue/cli-service": "5.0.8",
|
||||
"@vue/eslint-config-standard": "^8.0.1",
|
||||
"@vue/test-utils": "^1.3.6",
|
||||
"@vue/vue2-jest": "^27.0.0",
|
||||
"babel-jest": "^27.1.0",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^8.7.2",
|
||||
"eslint-config-corteza-client": "file:../../../lib/eslint-client",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"jest": "^27.1.0",
|
||||
"null-loader": "^4.0.1",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"rtlcss": "^4.3.0",
|
||||
"sass": "^1.85.1",
|
||||
"sass-loader": "^10",
|
||||
"sinon": "^9.2.4",
|
||||
"stylelint": "16.14.1",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"stylelint-scss": "^6.11.0",
|
||||
"stylelint-webpack-plugin": "^5.0.1",
|
||||
"vue-eslint-parser": "^9.4.3",
|
||||
"vue-template-compiler": "2.7.16",
|
||||
"webpack": "^5.97.1"
|
||||
}
|
||||
}
|
||||
6
client/web/reporter/postcss.config.js
Normal file
6
client/web/reporter/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
rtlcss: {},
|
||||
},
|
||||
}
|
||||
1
client/web/reporter/public/.gitignore
vendored
Normal file
1
client/web/reporter/public/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
config.js
|
||||
12
client/web/reporter/public/config.example.js
Normal file
12
client/web/reporter/public/config.example.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Corteza API location
|
||||
window.CortezaAPI = 'https://api.cortezaproject.your-domain.tld';
|
||||
|
||||
// CortezaAuth can be autoconfigured by replacing /api with /auth in CortezaAPI
|
||||
// or by appending /auth to the end of CortezaAPI string
|
||||
// When this is not possible and your configuration is more exotic you can set it
|
||||
// explicitly:
|
||||
// window.CortezaAuth = 'https://api.cortezaproject.your-domain.tld/auth';
|
||||
|
||||
// Configure CortezaWebapp when your web applications are not placed on the root.
|
||||
// This is autoconfigured from the value of <base> tag href attribute in most cases.
|
||||
// window.CortezaWebapp = 'https://cortezaproject.your-domain.tld';
|
||||
9
client/web/reporter/public/icon.svg
Normal file
9
client/web/reporter/public/icon.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.0075 -0.0058 36.9075 38.5121" width="32px" height="32px">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.st0{fill:#95B4AF;}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M2.5,9.4c1.6-2.9,4-5.3,6.9-6.9c3-1.7,6.3-2.5,9.8-2.5c3.9-0.1,7.8,1.1,11.1,3.2c3.2,2.2,5.5,5.3,6.6,9h-8.7 c-0.8-1.7-2-3.1-3.6-4.1c-1.7-0.9-3.5-1.4-5.4-1.3c-2.1,0-4.1,0.5-5.9,1.5c-1.7,1-3.1,2.6-4,4.4c-1,2.1-1.5,4.3-1.4,6.6 c0,2.3,0.4,4.5,1.4,6.6c0.9,1.8,2.3,3.3,4,4.4c1.8,1,3.8,1.6,5.9,1.5c1.9,0,3.8-0.4,5.4-1.4c1.6-1,2.9-2.4,3.6-4.1h8.7 c-1.1,3.7-3.5,6.8-6.6,9c-3.3,2.2-7.2,3.3-11.1,3.2c-3.4,0-6.8-0.8-9.8-2.5c-2.9-1.6-5.2-4-6.9-6.8c-1.7-3-2.6-6.5-2.5-10 C-0.1,15.9,0.8,12.4,2.5,9.4z" id="object-0" style="fill: rgb(255, 150, 97);"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 792 B |
34
client/web/reporter/public/index.html
Normal file
34
client/web/reporter/public/index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-color-mode="light" lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
href value should be modified when app is placed under subdir
|
||||
-->
|
||||
<base href="/" />
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" id="favicon" href="">
|
||||
<link rel="stylesheet" type="text/css" href="<%= BASE_URL %>custom.css">
|
||||
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
|
||||
<title><%= FLAVOUR %></title>
|
||||
</head>
|
||||
<body id="reporter">
|
||||
<noscript>
|
||||
<strong>
|
||||
JavaScript is disabled in your browser!
|
||||
</strong>
|
||||
<p>
|
||||
We're sorry but Corteza doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.
|
||||
</p>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
|
||||
<!-- contains cdns that will be injected at the head of the page -->
|
||||
<script src="<%= BASE_URL %>code-snippets.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
159
client/web/reporter/src/app.js
Normal file
159
client/web/reporter/src/app.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import './config-check'
|
||||
import './console-splash'
|
||||
|
||||
import './filters'
|
||||
import './plugins'
|
||||
import './mixins'
|
||||
import './components'
|
||||
import store from './store'
|
||||
|
||||
import { i18n, websocket } from '@cortezaproject/corteza-vue'
|
||||
|
||||
import router from './router'
|
||||
|
||||
export default (options = {}) => {
|
||||
options = {
|
||||
el: '#app',
|
||||
name: 'reporter',
|
||||
|
||||
template: '<router-view v-if="loaded && i18nLoaded" />',
|
||||
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
i18nLoaded: false,
|
||||
}),
|
||||
|
||||
async created () {
|
||||
this.$i18n.i18next.on('initialized', () => {
|
||||
this.i18nLoaded = true
|
||||
})
|
||||
|
||||
this.websocket()
|
||||
|
||||
return this.$auth.vue(this).handle().then(async ({ user }) => {
|
||||
// switch the favicon based on the settings
|
||||
await this.$Settings.init({ api: this.$SystemAPI }).then(() => {
|
||||
const icon = this.$Settings.attachment('ui.iconLogo') || '/icon.svg'
|
||||
|
||||
const favicon = document.getElementById('favicon')
|
||||
|
||||
if (favicon) {
|
||||
favicon.href = icon
|
||||
}
|
||||
})
|
||||
|
||||
if (user.meta.preferredLanguage) {
|
||||
// After user is authenticated, get his preferred language
|
||||
// and instruct i18next to change it
|
||||
this.$i18n.i18next.changeLanguage(user.meta.preferredLanguage)
|
||||
|
||||
/**
|
||||
* Let the API know what kind of language do we accept and send
|
||||
*/
|
||||
this.$ComposeAPI
|
||||
.setHeader('Accept-Language', user.meta.preferredLanguage)
|
||||
.setHeader('Content-Language', user.meta.preferredLanguage)
|
||||
}
|
||||
|
||||
// switch the webapp theme based on user preference
|
||||
if (user.meta.theme) {
|
||||
document.getElementsByTagName('html')[0].setAttribute('data-color-mode', user.meta.theme)
|
||||
}
|
||||
|
||||
// Load effective permissions
|
||||
this.$store.dispatch('rbac/load')
|
||||
|
||||
// Initialize notifications
|
||||
this.$store.dispatch('notifications/fetchNotifications')
|
||||
|
||||
this.loaded = true
|
||||
}).catch((err) => {
|
||||
if (err instanceof Error && err.message === 'Unauthenticated') {
|
||||
// user not logged-in,
|
||||
// start with authentication flow
|
||||
this.$auth.startAuthenticationFlow()
|
||||
return
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Registers event listener for websocket messages and
|
||||
* routes them depending on their type
|
||||
*/
|
||||
websocket () {
|
||||
// cross-link auth & websocket so that ws can use the right access token
|
||||
websocket.init(this)
|
||||
|
||||
// register event listener for messages
|
||||
this.$on('websocket-message', ({ data }) => {
|
||||
const msg = JSON.parse(data)
|
||||
switch (msg['@type']) {
|
||||
case 'notification':
|
||||
this.$store.dispatch('notifications/addNotification', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.read':
|
||||
this.$store.dispatch('notifications/updateReadNotification', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.unread':
|
||||
this.$store.dispatch('notifications/updateUnreadNotification', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.read.all':
|
||||
this.$store.dispatch('notifications/updateAllReadNotifications', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.unread.all':
|
||||
this.$store.dispatch('notifications/updateAllUnreadNotifications', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.delete':
|
||||
this.$store.dispatch('notifications/removeNotification', msg['@value'])
|
||||
break
|
||||
|
||||
case 'error':
|
||||
this.toastDanger('Websocket message with error', msg['@value'])
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
router,
|
||||
store,
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-reporter' },
|
||||
'builder',
|
||||
'create',
|
||||
'datasources',
|
||||
'display-element',
|
||||
'edit',
|
||||
'general',
|
||||
'list',
|
||||
'navigation',
|
||||
'notification',
|
||||
'notifications',
|
||||
'permissions',
|
||||
'scenarios',
|
||||
'sidebar',
|
||||
'view',
|
||||
),
|
||||
// Any additional options we want to merge
|
||||
...options,
|
||||
}
|
||||
|
||||
const app = new Vue(options)
|
||||
|
||||
// Simple HMR acceptance
|
||||
if (module.hot) {
|
||||
module.hot.accept()
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
BIN
client/web/reporter/src/assets/DisplayElements/Chart.png
Normal file
BIN
client/web/reporter/src/assets/DisplayElements/Chart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
client/web/reporter/src/assets/DisplayElements/Metric.png
Normal file
BIN
client/web/reporter/src/assets/DisplayElements/Metric.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
client/web/reporter/src/assets/DisplayElements/Placeholder.jpg
Normal file
BIN
client/web/reporter/src/assets/DisplayElements/Placeholder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
client/web/reporter/src/assets/DisplayElements/Table.png
Normal file
BIN
client/web/reporter/src/assets/DisplayElements/Table.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
client/web/reporter/src/assets/DisplayElements/Text.png
Normal file
BIN
client/web/reporter/src/assets/DisplayElements/Text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
4
client/web/reporter/src/assets/DisplayElements/index.js
Normal file
4
client/web/reporter/src/assets/DisplayElements/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const Text = require('./Text.png')
|
||||
export const Table = require('./Table.png')
|
||||
export const Chart = require('./Chart.png')
|
||||
export const Metric = require('./Metric.png')
|
||||
5
client/web/reporter/src/components/C3.js
Normal file
5
client/web/reporter/src/components/C3.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as report from './Report/C3'
|
||||
|
||||
export default {
|
||||
...report,
|
||||
}
|
||||
111
client/web/reporter/src/components/Common/ColumnPicker.vue
Normal file
111
client/web/reporter/src/components/Common/ColumnPicker.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<c-item-picker
|
||||
:value.sync="selected"
|
||||
:options="options"
|
||||
:labels="{
|
||||
searchPlaceholder: $t('list:searchPlaceholder'),
|
||||
availableItems: $t('available-columns'),
|
||||
selectAllItems: $t('select-all'),
|
||||
selectedItems: $t('selected-columns'),
|
||||
unselectAllItems: $t('unselect-all'),
|
||||
noItemsFound: $t('no-columns-found'),
|
||||
}"
|
||||
style="max-height: 40vh;"
|
||||
>
|
||||
<template
|
||||
#default="{ field }"
|
||||
>
|
||||
<b class="cursor-default text-dark">
|
||||
<template
|
||||
v-if="field.label"
|
||||
>
|
||||
{{ field.label }} ({{ field.name }})
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
{{ field.name }}
|
||||
</template>
|
||||
</b>
|
||||
</template>
|
||||
</c-item-picker>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CItemPicker } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'builder',
|
||||
},
|
||||
|
||||
components: {
|
||||
CItemPicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
// source of fields
|
||||
allItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
// array of objects, list of all selected fields
|
||||
selectedItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
enableSorting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Converting old-way of passing data to picker components
|
||||
* where arrays of objects where exchanged and stored
|
||||
*
|
||||
* Item picker does not do that any more so we need to adapt
|
||||
* whatever we get from the outside to array of picked field names
|
||||
* and vice versa!
|
||||
*/
|
||||
selected: {
|
||||
get () {
|
||||
// Only need names of the fields
|
||||
return this.selectedItems.map(({ name }) => name)
|
||||
},
|
||||
|
||||
set (selected) {
|
||||
this.$emit('update:selected-items', selected)
|
||||
},
|
||||
},
|
||||
|
||||
options () {
|
||||
const fields = this.allItems
|
||||
|
||||
if (this.enableSorting) {
|
||||
fields.sort((a, b) => a.label.localeCompare(b.label))
|
||||
}
|
||||
|
||||
return [...fields,
|
||||
].map(field => ({
|
||||
value: field.name,
|
||||
text: [
|
||||
field.name,
|
||||
field.label,
|
||||
field.kind,
|
||||
field.system ? 'system' : '',
|
||||
].join(' '),
|
||||
field,
|
||||
}))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
46
client/web/reporter/src/components/Common/ColumnSelector.vue
Normal file
46
client/web/reporter/src/components/Common/ColumnSelector.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<c-input-select
|
||||
:value="value"
|
||||
:options="columns"
|
||||
:get-option-label="getColumnLabel"
|
||||
:get-option-key="getOptionKey"
|
||||
:placeholder="$t('general:label.none')"
|
||||
:reduce="r => r.name"
|
||||
append-to-body
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getColumnLabel ({ name, label }) {
|
||||
return `${label} (${name})`
|
||||
},
|
||||
|
||||
getOptionKey ({ name }) {
|
||||
return name
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.column-selector {
|
||||
min-width: 15vw !important;
|
||||
max-width: 150px !important;
|
||||
}
|
||||
</style>
|
||||
108
client/web/reporter/src/components/Common/Configurator.vue
Normal file
108
client/web/reporter/src/components/Common/Configurator.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<b-container
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="3"
|
||||
>
|
||||
<b-list-group>
|
||||
<draggable
|
||||
:list.sync="items"
|
||||
:disabled="!draggable"
|
||||
group="items"
|
||||
handle=".grab"
|
||||
>
|
||||
<b-list-group-item
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
button
|
||||
:active="currentIndex ? currentIndex === index : index === 0"
|
||||
class="item d-flex align-items-center justify-content-between"
|
||||
@click="$emit('select', index)"
|
||||
>
|
||||
<slot
|
||||
name="label"
|
||||
:item="item"
|
||||
/>
|
||||
</b-list-group-item>
|
||||
</draggable>
|
||||
|
||||
<b-list-group-item
|
||||
button
|
||||
class="text-primary rounded-top"
|
||||
:class="{ 'border-top-0': items.length }"
|
||||
@click="$emit('add')"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'plus']"
|
||||
size="sm"
|
||||
class="mr-1"
|
||||
/>
|
||||
{{ $t('general:label.add') }}
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
v-if="currentIndex !== undefined"
|
||||
cols="9"
|
||||
>
|
||||
<slot
|
||||
name="configurator"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
variant="danger"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
:borderless="false"
|
||||
:text="$t('general:label.delete')"
|
||||
class="d-flex"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Draggable,
|
||||
},
|
||||
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.item {
|
||||
.grab {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.grab {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
456
client/web/reporter/src/components/Common/Prefilter.vue
Normal file
456
client/web/reporter/src/components/Common/Prefilter.vue
Normal file
@@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<c-form-table-wrapper hide-add-button>
|
||||
<b-table-simple
|
||||
v-if="render && filter.ref"
|
||||
responsive
|
||||
borderless
|
||||
small
|
||||
>
|
||||
<template
|
||||
v-for="(group, groupIndex) in filter.args"
|
||||
>
|
||||
<tr
|
||||
v-for="(arg, argIndex) in group.args[0].args"
|
||||
:key="`${groupIndex}-${argIndex}`"
|
||||
>
|
||||
<td
|
||||
class="fit text-center align-middle pl-0"
|
||||
>
|
||||
<b-form-select
|
||||
v-if="argIndex === 1"
|
||||
v-model="group.args[0].ref"
|
||||
:options="conditions"
|
||||
class="w-auto"
|
||||
@change="reRender()"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="px-3"
|
||||
style="min-width: 60px;"
|
||||
>
|
||||
{{ argIndex === 0 ? 'Where' : `${group.args[0].ref[0].toUpperCase() + group.args[0].ref.slice(1).toLowerCase()}` }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<template v-if="Object.keys(arg).includes('raw')">
|
||||
<td>
|
||||
<b-input-group>
|
||||
<b-input-group-prepend>
|
||||
<b-button
|
||||
variant="outline-primary"
|
||||
class="d-flex justify-content-center align-items-center"
|
||||
@click="toggleMode(groupIndex, argIndex)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'filter']"
|
||||
size="sm"
|
||||
/>
|
||||
</b-button>
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-form-input
|
||||
v-model="arg.raw"
|
||||
:placeholder="$t('builder:filter-expression')"
|
||||
/>
|
||||
</b-input-group>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template v-else-if="group">
|
||||
<td>
|
||||
<b-input-group>
|
||||
<b-input-group-prepend>
|
||||
<b-button
|
||||
variant="primary"
|
||||
class="d-flex justify-content-center align-items-center"
|
||||
@click="toggleMode(groupIndex, argIndex)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'filter']"
|
||||
size="sm"
|
||||
/>
|
||||
</b-button>
|
||||
</b-input-group-prepend>
|
||||
|
||||
<template v-if="group.args[0].args[argIndex].args[0].args[0].value && getColumnData(group.args[0].args[argIndex].args[0].args[1]).multivalue">
|
||||
<column-selector
|
||||
:columns="columns"
|
||||
:value="group.args[0].args[argIndex].args[0].args[1].symbol"
|
||||
@input="setType(groupIndex, argIndex, $event, group.args[0].args[argIndex].args[0].args[0].value['@value'])"
|
||||
/>
|
||||
|
||||
<b-form-select
|
||||
v-model="group.args[0].args[argIndex].args[0].ref"
|
||||
:options="getOperators(getColumnData(group.args[0].args[argIndex].args[0].args[1]))"
|
||||
class="border-left-0"
|
||||
style="max-width: 120px;"
|
||||
/>
|
||||
|
||||
<b-form-input
|
||||
v-model="group.args[0].args[argIndex].args[0].args[0].value['@value']"
|
||||
:placeholder="$t('builder:value')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<column-selector
|
||||
:value="group.args[0].args[argIndex].args[0].args[0].symbol"
|
||||
:columns="columns"
|
||||
@input="setType(groupIndex, argIndex, $event, group.args[0].args[argIndex].args[0].args[1].value['@value'])"
|
||||
/>
|
||||
|
||||
<b-form-select
|
||||
v-model="group.args[0].args[argIndex].args[0].ref"
|
||||
:options="getOperators(getColumnData(group.args[0].args[argIndex].args[0].args[0]))"
|
||||
class="border-left-0"
|
||||
style="max-width: 120px;"
|
||||
/>
|
||||
|
||||
<b-form-input
|
||||
v-model="group.args[0].args[argIndex].args[0].args[1].value['@value']"
|
||||
:placeholder="$t('builder:value')"
|
||||
/>
|
||||
</template>
|
||||
</b-input-group>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<td
|
||||
class="fit text-center align-middle pl-2 pr-0"
|
||||
>
|
||||
<c-input-confirm
|
||||
show-icon
|
||||
@confirmed="deleteFilter(groupIndex, argIndex)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
:key="`${groupIndex}-add`"
|
||||
>
|
||||
<td
|
||||
class="fit align-middle pl-0"
|
||||
:class="{ 'text-center': group.args[0].args && group.args[0].args.length }"
|
||||
>
|
||||
<b-button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="mt-1"
|
||||
@click="addFilter(groupIndex)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'plus']"
|
||||
size="sm"
|
||||
class="mr-1"
|
||||
/>
|
||||
{{ $t('general:label.add') }}
|
||||
</b-button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
v-if="group.args[0].args && group.args[0].args.length"
|
||||
:key="`${groupIndex}-addGroup`"
|
||||
>
|
||||
<td
|
||||
colspan="100%"
|
||||
class="p-0 filter-border text-center"
|
||||
:class="{ 'pb-1': groupIndex < filter.args.length - 1 }"
|
||||
>
|
||||
<b-form-select
|
||||
v-if="groupIndex < filter.args.length - 1"
|
||||
v-model="filter.ref"
|
||||
:options="conditions"
|
||||
class="w-auto"
|
||||
@change="reRender()"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
v-else
|
||||
variant="outline-primary"
|
||||
class="btn-add-group bg-white py-2 px-3"
|
||||
@click="addGroup()"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'plus']"
|
||||
class="h6 mb-0 "
|
||||
/>
|
||||
</b-button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</b-table-simple>
|
||||
|
||||
<tr
|
||||
v-else
|
||||
>
|
||||
<b-button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@click="initFilter()"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'plus']"
|
||||
size="sm"
|
||||
class="mr-1"
|
||||
/>
|
||||
{{ $t('general:label.add') }}
|
||||
</b-button>
|
||||
</tr>
|
||||
</c-form-table-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColumnSelector from 'corteza-webapp-reporter/src/components/Common/ColumnSelector.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnSelector,
|
||||
},
|
||||
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
render: true,
|
||||
|
||||
defaultFilter: {
|
||||
ref: 'group',
|
||||
// raw: ''
|
||||
args: [{
|
||||
ref: 'eq',
|
||||
args: [
|
||||
{ symbol: '' },
|
||||
{ value: { '@type': 'String', '@value': '' } },
|
||||
],
|
||||
}],
|
||||
},
|
||||
|
||||
conditions: [
|
||||
{ value: 'and', text: this.$t('general:label.and') },
|
||||
{ value: 'or', text: this.$t('general:label.or') },
|
||||
],
|
||||
|
||||
operators: [
|
||||
{
|
||||
value: 'eq',
|
||||
text: this.$t('builder:filter.operators.equal'),
|
||||
isMulti: false,
|
||||
},
|
||||
{
|
||||
value: 'ne',
|
||||
text: this.$t('builder:filter.operators.notEqual'),
|
||||
isMulti: false,
|
||||
},
|
||||
{
|
||||
value: 'lt',
|
||||
text: this.$t('builder:filter.operators.lessThan'),
|
||||
isMulti: false,
|
||||
},
|
||||
{
|
||||
value: 'le',
|
||||
text: this.$t('builder:filter.operators.lessThanEqualTo'),
|
||||
isMulti: false,
|
||||
},
|
||||
{
|
||||
value: 'gt',
|
||||
text: this.$t('builder:filter.operators.greaterThan'),
|
||||
isMulti: false,
|
||||
},
|
||||
{
|
||||
value: 'ge',
|
||||
text: this.$t('builder:filter.operators.greaterThanEqualTo'),
|
||||
isMulti: false,
|
||||
},
|
||||
{
|
||||
value: 'in',
|
||||
text: this.$t('builder:filter.operators.contains'),
|
||||
isMulti: true,
|
||||
},
|
||||
{
|
||||
value: 'nin',
|
||||
text: this.$t('builder:filter.operators.notContains'),
|
||||
isMulti: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
initFilter () {
|
||||
this.filter.ref = 'and'
|
||||
this.filter.args = []
|
||||
this.addGroup()
|
||||
},
|
||||
|
||||
addGroup () {
|
||||
if (this.filter.args) {
|
||||
this.filter.args.push({
|
||||
ref: 'group',
|
||||
args: [
|
||||
{
|
||||
ref: 'or',
|
||||
args: [{
|
||||
ref: 'group',
|
||||
// raw: ''
|
||||
args: [{
|
||||
ref: 'eq',
|
||||
args: [
|
||||
{ symbol: '' },
|
||||
{ value: { '@type': '', '@value': '' } },
|
||||
],
|
||||
}],
|
||||
}],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
this.reRender()
|
||||
},
|
||||
|
||||
addFilter (groupIndex) {
|
||||
if (!this.filter.args[groupIndex].args[0].args) {
|
||||
this.filter.args[groupIndex].args[0].args = []
|
||||
}
|
||||
|
||||
this.filter.args[groupIndex].args[0].args.push({
|
||||
ref: 'group',
|
||||
// raw: ''
|
||||
args: [{
|
||||
ref: 'eq',
|
||||
args: [
|
||||
{ symbol: '' },
|
||||
{ value: { '@type': '', '@value': '' } },
|
||||
],
|
||||
}],
|
||||
})
|
||||
|
||||
this.reRender()
|
||||
},
|
||||
|
||||
deleteFilter (groupIndex, argIndex) {
|
||||
const { args } = this.filter.args[groupIndex].args[0]
|
||||
|
||||
if (args) {
|
||||
// If last group and last filter, set filter to default
|
||||
if (this.filter.args.length === 1 && args.length === 1) {
|
||||
delete this.filter.ref
|
||||
delete this.filter.args
|
||||
} else if (args.length === 1) {
|
||||
// If only one left in group, remove group
|
||||
this.filter.args.splice(groupIndex, 1)
|
||||
} else {
|
||||
// Remove filter from group
|
||||
args.splice(argIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.reRender()
|
||||
},
|
||||
|
||||
toggleMode (groupIndex, argIndex) {
|
||||
const { args } = this.filter.args[groupIndex].args[0]
|
||||
|
||||
if (args[argIndex]) {
|
||||
if (Object.keys(args[argIndex]).includes('raw')) {
|
||||
args[argIndex].args = [{
|
||||
ref: 'eq',
|
||||
args: [
|
||||
{ symbol: '' },
|
||||
{ value: { '@type': '', '@value': '' } },
|
||||
],
|
||||
}]
|
||||
|
||||
delete args[argIndex].raw
|
||||
} else {
|
||||
args[argIndex].raw = ''
|
||||
|
||||
delete args[argIndex].args
|
||||
}
|
||||
|
||||
this.reRender()
|
||||
}
|
||||
},
|
||||
|
||||
setType (groupIndex, argIndex, symbol, value) {
|
||||
if (!this.filter.args[groupIndex].args[0].args[argIndex]) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get type
|
||||
const { kind = '', multivalue } = this.columns.find(({ name }) => name === symbol) || {}
|
||||
|
||||
// Set type
|
||||
if (multivalue) {
|
||||
this.filter.args[groupIndex].args[0].args[argIndex].args[0].args[0] = { value: { '@type': kind, '@value': value } }
|
||||
this.filter.args[groupIndex].args[0].args[argIndex].args[0].args[1] = { symbol }
|
||||
this.filter.args[groupIndex].args[0].args[argIndex].args[0].ref = 'in'
|
||||
} else {
|
||||
this.filter.args[groupIndex].args[0].args[argIndex].args[0].args[0] = { symbol }
|
||||
this.filter.args[groupIndex].args[0].args[argIndex].args[0].args[1] = { value: { '@type': kind, '@value': value } }
|
||||
this.filter.args[groupIndex].args[0].args[argIndex].args[0].ref = 'eq'
|
||||
}
|
||||
|
||||
this.reRender()
|
||||
},
|
||||
|
||||
reRender () {
|
||||
this.render = false
|
||||
this.$nextTick().then(() => {
|
||||
this.render = true
|
||||
})
|
||||
},
|
||||
|
||||
getColumnData (group) {
|
||||
return this.columns.find(({ name }) => name === group.symbol)
|
||||
},
|
||||
|
||||
getOperators (column) {
|
||||
return column ? this.operators.filter(value => value.isMulti === column.multivalue) : this.operators
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table td.fit,
|
||||
.table th.fit {
|
||||
white-space: nowrap;
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
.btn-add-group {
|
||||
&:hover, &:active {
|
||||
background-color: var(--primary) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-border {
|
||||
background-image: linear-gradient(to left, lightgray, lightgray);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 1px;
|
||||
background-position: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.prefilter .column-selector {
|
||||
.vs__dropdown-toggle {
|
||||
border-right: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
76
client/web/reporter/src/components/Common/Selector.vue
Normal file
76
client/web/reporter/src/components/Common/Selector.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<b-container fluid>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="3"
|
||||
>
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
v-for="element in items"
|
||||
:key="element.kind"
|
||||
button
|
||||
class="text-truncate"
|
||||
@mouseover="currentValue = element.value"
|
||||
@click="$emit('select', element.kind)"
|
||||
>
|
||||
{{ element.label }}
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
v-if="currentValue"
|
||||
cols="9"
|
||||
:class="{ 'my-auto': displayMode === 'image' }"
|
||||
>
|
||||
<b-img
|
||||
v-if="displayMode === 'image'"
|
||||
fluid
|
||||
thumbnail
|
||||
:src="currentValue"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-else-if="displayMode === 'text'"
|
||||
>
|
||||
{{ currentValue }}
|
||||
</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
// { label, kind, value }
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
// image or text
|
||||
displayMode: {
|
||||
type: String,
|
||||
default: 'image',
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
currentValue: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
items: {
|
||||
immediate: true,
|
||||
handler (items = []) {
|
||||
const { value } = items[0]
|
||||
|
||||
if (value) {
|
||||
this.currentValue = value
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
115
client/web/reporter/src/components/EditorToolbar.vue
Normal file
115
client/web/reporter/src/components/EditorToolbar.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<c-toolbar
|
||||
class="bg-white shadow border-top"
|
||||
>
|
||||
<template #start>
|
||||
<b-button
|
||||
v-if="backLink"
|
||||
data-test-id="button-back"
|
||||
variant="link"
|
||||
:to="backLink"
|
||||
class="d-flex align-items-center text-dark back text-nowrap gap-1 text-decoration-none"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'chevron-left']"
|
||||
class="back-icon"
|
||||
/>
|
||||
{{ $t('general:label.back') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #center>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<c-input-confirm
|
||||
v-if="!hideDelete"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
variant="danger"
|
||||
:disabled="deleteDisabled || processingDelete || processing"
|
||||
:processing="processingDelete"
|
||||
:text="$t('general:label.delete')"
|
||||
:borderless="false"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
data-test-id="button-clone"
|
||||
:disabled="cloneDisabled || processingClone || processing"
|
||||
:processing="processingClone"
|
||||
variant="light"
|
||||
:text="$t('general:label.clone')"
|
||||
class="text-nowrap"
|
||||
size="lg"
|
||||
@submit="$emit('clone')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
data-test-id="button-save"
|
||||
:disabled="saveDisabled || processingSave || processing"
|
||||
:processing="processingSave"
|
||||
:text="$t('general:label.save')"
|
||||
size="lg"
|
||||
@submit="$emit('save')"
|
||||
/>
|
||||
</template>
|
||||
</c-toolbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CToolbar } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CToolbar,
|
||||
},
|
||||
|
||||
props: {
|
||||
backLink: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ name: 'root' }),
|
||||
},
|
||||
|
||||
hideDelete: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
deleteDisabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
hideSave: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
saveDisabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
cloneDisabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
processingDelete: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
processingSave: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
|
||||
processingClone: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,804 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="mb-3"
|
||||
>
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('chart.configurator.general') }}
|
||||
</h5>
|
||||
|
||||
<b-row
|
||||
align-v="stretch"
|
||||
>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.type')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
:value="displayElementOptions.type"
|
||||
:options="chartTypes"
|
||||
@change="typeChanged"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.chart-title')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.title"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.color-scheme')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<c-input-select
|
||||
v-model="options.colorScheme"
|
||||
:options="colorSchemes"
|
||||
:get-option-key="getOptionKey"
|
||||
:reduce="cs => cs.value"
|
||||
:placeholder="$t('general:label.default')"
|
||||
>
|
||||
<template #option="option">
|
||||
<p
|
||||
class="mb-1"
|
||||
>
|
||||
{{ option.label }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-for="(color, index) in option.colors"
|
||||
:key="`${option.value}-${index}`"
|
||||
:style="`background: ${color};`"
|
||||
class="d-inline-block color-box mr-1 mb-1"
|
||||
/>
|
||||
</template>
|
||||
</c-input-select>
|
||||
|
||||
<template
|
||||
v-if="currentColorScheme"
|
||||
>
|
||||
<div
|
||||
v-for="(color, index) in currentColorScheme.colors"
|
||||
:key="`${currentColorScheme.value}-${index}`"
|
||||
:style="`background: ${color};`"
|
||||
class="d-inline-block color-box mr-1"
|
||||
/>
|
||||
</template>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
class="d-flex flex-column justify-content-center"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="options.noAnimation"
|
||||
:value="false || undefined"
|
||||
:unchecked-value="true"
|
||||
switch
|
||||
class="mt-3 pt-2"
|
||||
>
|
||||
{{ $t('chart.configurator.animation.enabled') }}
|
||||
</b-form-checkbox>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="options.source"
|
||||
class="mb-3"
|
||||
>
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('chart.configurator.data') }}
|
||||
</h5>
|
||||
|
||||
<b-form-group
|
||||
v-if="options.labelColumn !== undefined"
|
||||
:label="$t('chart.configurator.label-column')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<column-selector
|
||||
v-model="options.labelColumn"
|
||||
:columns="labelColumns"
|
||||
style="min-width: 100% !important;"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="options.dataColumns && columns.length"
|
||||
:label="$t('chart.configurator.data-columns')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<column-picker
|
||||
:all-items="dataColumns"
|
||||
:selected-items="options.dataColumns"
|
||||
class="d-flex flex-column"
|
||||
@update:selected-items="updateDataColumns"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<div
|
||||
v-if="['bar', 'line'].includes(options.type)"
|
||||
>
|
||||
<hr>
|
||||
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('chart.configurator.metrics.label') }}
|
||||
</h5>
|
||||
|
||||
<b-form-group
|
||||
v-if="options.dataColumns && options.dataColumns.length"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-form-table-wrapper
|
||||
v-for="(column, index) in metricColumns"
|
||||
:key="index"
|
||||
hide-add-button
|
||||
class="mb-3"
|
||||
>
|
||||
<h6 class="mb-3">
|
||||
{{ column.label }}
|
||||
</h6>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.metrics.label-field.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="column.label"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.metrics.stack.label')"
|
||||
:description="$t('chart.configurator.metrics.stack.description')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="column.stack"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</c-form-table-wrapper>
|
||||
</b-form-group>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="['bar', 'line'].includes(options.type)"
|
||||
>
|
||||
<hr>
|
||||
|
||||
<div
|
||||
class="mb-3"
|
||||
>
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('chart.configurator.x-axis.name') }}
|
||||
</h5>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.x-axis.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.xAxis.label"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.x-axis.type')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="options.xAxis.type"
|
||||
:options="AxisTypes"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
value=""
|
||||
>
|
||||
{{ $t('chart.configurator.default') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.default-value')"
|
||||
label-class="text-primary"
|
||||
class="mb-1"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.xAxis.defaultValue"
|
||||
:disabled="options.xAxis.skipMissing"
|
||||
:type="options.xAxis.type === 'time' ? 'date' : 'text'"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="options.xAxis.skipMissing"
|
||||
class="mb-3"
|
||||
>
|
||||
{{ $t('chart.configurator.skip-missing-values') }}
|
||||
</b-form-checkbox>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.x-axis.labelRotation.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-1"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.xAxis.labelRotation"
|
||||
type="number"
|
||||
number
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('chart.configurator.y-axis.name') }}
|
||||
</h5>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.y-axis.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.yAxis.label"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.y-axis.labelPosition.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="options.yAxis.labelPosition"
|
||||
:options="axisLabelPositions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.y-axis.labelRotation.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-1"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.yAxis.labelRotation"
|
||||
type="number"
|
||||
number
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.value.min')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.yAxis.min"
|
||||
type="number"
|
||||
number
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.value.max')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.yAxis.max"
|
||||
type="number"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.step-size')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.yAxis.stepSize"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="options.yAxis.type"
|
||||
value="logarithmic"
|
||||
unchecked-value="linear"
|
||||
>
|
||||
{{ $t('chart.configurator.logarithmic-scale') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="options.yAxis.beginAtZero"
|
||||
>
|
||||
{{ $t('chart.configurator.begin-axis-at-zero') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="options.yAxis.position"
|
||||
value="right"
|
||||
unchecked-value="left"
|
||||
>
|
||||
{{ $t('chart.configurator.place-axis-on-right-side') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('chart.configurator.legend.name') }}
|
||||
</h5>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="options.legend.hide"
|
||||
class="mb-3"
|
||||
>
|
||||
{{ $t('chart.configurator.legend.hide') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-row v-if="!options.legend.hide">
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.legend.align.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-1"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="options.legend.align"
|
||||
:options="alignments"
|
||||
:disabled="!options.legend.position.default"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.legend.orientation.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-1"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="options.legend.orientation"
|
||||
:options="orientations"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="options.legend.position.default"
|
||||
:class="{ 'mb-3': !options.legend.position.default }"
|
||||
>
|
||||
{{ $t('chart.configurator.legend.position.default') }}
|
||||
</b-form-checkbox>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="options.legend.scrollable"
|
||||
:disabled="options.legend.orientation !== 'horizontal'"
|
||||
>
|
||||
{{ $t('chart.configurator.legend.scrollable') }}
|
||||
</b-form-checkbox>
|
||||
</b-col>
|
||||
|
||||
<template
|
||||
v-if="!options.legend.position.default"
|
||||
>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.legend.position.top')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.legend.position.top"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.legend.position.right')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.legend.position.right"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.legend.position.bottom')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.legend.position.bottom"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.legend.position.left')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.legend.position.left"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col>
|
||||
<small>{{ $t('chart.configurator.position-description') }}</small>
|
||||
</b-col>
|
||||
</template>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('chart.configurator.tooltips.name') }}
|
||||
</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-checkbox
|
||||
v-model="options.tooltips.showAlways"
|
||||
>
|
||||
{{ $t('chart.configurator.tooltips.show.always') }}
|
||||
</b-form-checkbox>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-2">
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('chart.configurator.offset.name') }}
|
||||
</h5>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="options.offset.default"
|
||||
class="mb-3"
|
||||
>
|
||||
{{ $t('chart.configurator.offset.default') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-row v-if="!options.offset.default">
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.offset.top')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.offset.top"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.offset.right')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.offset.right"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.offset.bottom')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.offset.bottom"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('chart.configurator.offset.left')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="options.offset.left"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<small>{{ $t('chart.configurator.position-description') }}</small>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ColumnSelector from 'corteza-webapp-reporter/src/components/Common/ColumnSelector.vue'
|
||||
import ColumnPicker from 'corteza-webapp-reporter/src/components/Common/ColumnPicker'
|
||||
import { reporter, shared } from '@cortezaproject/corteza-js'
|
||||
const { colorschemes } = shared
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'display-element',
|
||||
},
|
||||
|
||||
name: 'ChartConfigurator',
|
||||
|
||||
components: {
|
||||
ColumnSelector,
|
||||
ColumnPicker,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
colorSchemes: [],
|
||||
|
||||
allowedLabelKinds: [
|
||||
'Date',
|
||||
'DateTime',
|
||||
'Select',
|
||||
'Number',
|
||||
'Bool',
|
||||
'String',
|
||||
'Record',
|
||||
'User',
|
||||
],
|
||||
|
||||
axisLabelPositions: [
|
||||
{ value: 'end', text: this.$t('chart.configurator.y-axis.labelPosition.top') },
|
||||
{ value: 'center', text: this.$t('chart.configurator.y-axis.labelPosition.center') },
|
||||
{ value: 'start', text: this.$t('chart.configurator.y-axis.labelPosition.bottom') },
|
||||
],
|
||||
|
||||
orientations: [
|
||||
{ value: 'horizontal', text: this.$t('chart.configurator.legend.orientation.horizontal') },
|
||||
{ value: 'vertical', text: this.$t('chart.configurator.legend.orientation.vertical') },
|
||||
],
|
||||
|
||||
alignments: [
|
||||
{ value: 'left', text: this.$t('chart.configurator.legend.align.left') },
|
||||
{ value: 'center', text: this.$t('chart.configurator.legend.align.center') },
|
||||
{ value: 'right', text: this.$t('chart.configurator.legend.align.right') },
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
mergedColumns () {
|
||||
return [].concat.apply([], this.columns)
|
||||
},
|
||||
|
||||
chartTypes () {
|
||||
const types = [
|
||||
{ value: 'bar', text: this.$t('chart.configurator.types.bar') },
|
||||
{ value: 'line', text: this.$t('chart.configurator.types.line') },
|
||||
{ value: 'pie', text: this.$t('chart.configurator.types.pie') },
|
||||
{ value: 'doughnut', text: this.$t('chart.configurator.types.doughnut') },
|
||||
]
|
||||
|
||||
if (this.datasource && this.datasource.step.aggregate) {
|
||||
types.push({ value: 'funnel', text: this.$t('chart.configurator.types.funnel') })
|
||||
}
|
||||
|
||||
return types
|
||||
},
|
||||
|
||||
AxisTypes () {
|
||||
return [
|
||||
{ value: 'time', text: this.$t('chart.configurator.time.label') },
|
||||
{ value: 'category', text: 'Category' },
|
||||
]
|
||||
},
|
||||
|
||||
timeUnits () {
|
||||
return [
|
||||
{ value: 'day', text: this.$t('chart.configurator.time.unit.types.date') },
|
||||
{ value: 'week', text: this.$t('chart.configurator.time.unit.types.week') },
|
||||
{ value: 'month', text: this.$t('chart.configurator.time.unit.types.month') },
|
||||
{ value: 'quarter', text: this.$t('chart.configurator.time.unit.types.quarter') },
|
||||
{ value: 'year', text: this.$t('chart.configurator.time.unit.types.year') },
|
||||
]
|
||||
},
|
||||
|
||||
labelColumns () {
|
||||
const columns = this.columns.length ? this.columns[0] : []
|
||||
return columns.filter(({ kind }) => this.allowedLabelKinds.includes(kind))
|
||||
},
|
||||
|
||||
dataColumns () {
|
||||
return [
|
||||
...this.mergedColumns.filter(({ kind }) => kind === 'Number'),
|
||||
].sort((a, b) => a.label.localeCompare(b.label))
|
||||
},
|
||||
|
||||
currentColorScheme () {
|
||||
return this.colorSchemes.find(({ value }) => value === this.options.colorScheme)
|
||||
},
|
||||
|
||||
metricColumns () {
|
||||
return this.options.dataColumns
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
const capitalize = w => `${w[0].toUpperCase()}${w.slice(1)}`
|
||||
const splicer = sc => {
|
||||
const rr = (/(\D+)(\d+)$/gi).exec(sc)
|
||||
return {
|
||||
label: rr[1],
|
||||
count: rr[2],
|
||||
}
|
||||
}
|
||||
|
||||
const rr = []
|
||||
for (const g in colorschemes) {
|
||||
for (const sc in colorschemes[g]) {
|
||||
const gn = splicer(sc)
|
||||
|
||||
rr.push({
|
||||
label: `${capitalize(g)}: ${capitalize(gn.label)}`,
|
||||
colors: [...colorschemes[g][sc]],
|
||||
value: `${g}.${sc}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.colorSchemes = rr
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateDataColumns (columns) {
|
||||
const result = columns.map((c) => this.dataColumns.find(({ name }) => name === c))
|
||||
|
||||
result.forEach((c, idx) => {
|
||||
const initialCol = this.options.dataColumns.find(({ name }) => name === c.name)
|
||||
if (initialCol) result[idx] = initialCol
|
||||
})
|
||||
|
||||
this.options.dataColumns = result
|
||||
},
|
||||
|
||||
setColorscheme (colorscheme) {
|
||||
this.options.colorScheme = (colorscheme || {}).value || ''
|
||||
},
|
||||
|
||||
typeChanged (type) {
|
||||
this.options = reporter.ChartOptionsMaker({ ...this.options, type })
|
||||
},
|
||||
|
||||
getOptionKey ({ value }) {
|
||||
return value
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.color-box {
|
||||
width: 28px;
|
||||
height: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
v-if="options.valueColumn !== undefined"
|
||||
:label="$t('label-column')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<column-selector
|
||||
v-model="options.valueColumn"
|
||||
:columns="valueColumns"
|
||||
style="min-width: 100% !important;"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('format')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.format"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('prefix')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.prefix"
|
||||
placeholder="$"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('suffix')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="options.suffix"
|
||||
placeholder="USD/mo"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('color.text')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-color-picker
|
||||
v-model="options.color"
|
||||
:translations="{
|
||||
modalTitle: $t('color.picker'),
|
||||
light: $t('general:themes.labels.light'),
|
||||
dark: $t('general:themes.labels.dark'),
|
||||
cancelBtnLabel: $t('general:label.cancel'),
|
||||
saveBtnLabel: $t('general:label.saveAndClose')
|
||||
}"
|
||||
:theme-settings="themeSettings"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('color.background')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-color-picker
|
||||
v-model="options.backgroundColor"
|
||||
:translations="{
|
||||
modalTitle: $t('color.picker'),
|
||||
light: $t('general:themes.labels.light'),
|
||||
dark: $t('general:themes.labels.dark'),
|
||||
cancelBtnLabel: $t('general:label.cancel'),
|
||||
saveBtnLabel: $t('general:label.saveAndClose')
|
||||
}"
|
||||
:theme-settings="themeSettings"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ColumnSelector from 'corteza-webapp-reporter/src/components/Common/ColumnSelector.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputColorPicker } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'display-element',
|
||||
keyPrefix: 'metric.configurator',
|
||||
},
|
||||
|
||||
components: {
|
||||
ColumnSelector,
|
||||
CInputColorPicker,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
valueColumns () {
|
||||
const columns = this.columns.length ? this.columns[0] : []
|
||||
return [
|
||||
...columns.filter(({ kind }) => ['Number'].includes(kind)),
|
||||
].sort((a, b) => a.label.localeCompare(b.label))
|
||||
},
|
||||
|
||||
themeSettings () {
|
||||
return this.$Settings.get('ui.studio.themes', [])
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="options"
|
||||
>
|
||||
<div
|
||||
class="mb-3"
|
||||
>
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('display-element:table.configurator.general') }}
|
||||
</h5>
|
||||
|
||||
<b-row no-gutters>
|
||||
<b-col class="pr-3">
|
||||
<b-form-group
|
||||
:label="$t('display-element:table.configurator.table.variant')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="options.tableVariant"
|
||||
:options="tableVariants"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('display-element:table.configurator.head-variant')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-radio-group
|
||||
v-model="options.headVariant"
|
||||
class="mt-lg-2"
|
||||
>
|
||||
<b-form-radio
|
||||
:value="null"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.none') }}
|
||||
</b-form-radio>
|
||||
<b-form-radio
|
||||
value="light"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.light') }}
|
||||
</b-form-radio>
|
||||
<b-form-radio
|
||||
value="dark"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.dark') }}
|
||||
</b-form-radio>
|
||||
</b-form-radio-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('display-element:table.configurator.table.options.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="options.striped"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.striped') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="options.bordered"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.bordered') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="options.borderless"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.borderless') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="options.small"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.small') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="options.hover"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.hover') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="options.dark"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.dark') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="options.responsive"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.responsive') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="options.fixed"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.fixed') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="options.noCollapse"
|
||||
inline
|
||||
>
|
||||
{{ $t('display-element:table.configurator.table.options.no-collapse') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div
|
||||
class="mb-3"
|
||||
>
|
||||
<h5 class="text-primary mb-2">
|
||||
{{ $t('display-element:table.configurator.data') }}
|
||||
</h5>
|
||||
|
||||
<b-form-group
|
||||
v-if="options.datasources.length > 1"
|
||||
:label="$t('display-element:table.configurator.joined-datasource-handling')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="currentConfigurableDatasourceName"
|
||||
:options="options.datasources"
|
||||
text-field="name"
|
||||
value-field="name"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="currentConfigurableDatasourceName && currentColumns.length"
|
||||
:label="$t('display-element:table.configurator.columns')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<column-picker
|
||||
:all-items="currentColumns"
|
||||
:selected-items="currentSelectedColumns"
|
||||
class="d-flex flex-column"
|
||||
@update:selected-items="updateSelectedColumns"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ColumnPicker from 'corteza-webapp-reporter/src/components/Common/ColumnPicker'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnPicker,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
currentConfigurableDatasourceName: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
tableVariants () {
|
||||
return [
|
||||
{ value: '', text: this.$t('display-element:table.configurator.none') },
|
||||
{ value: 'primary', text: this.$t('display-element:table.configurator.table.variants.primary') },
|
||||
{ value: 'secondary', text: this.$t('display-element:table.configurator.table.variants.secondary') },
|
||||
{ value: 'info', text: this.$t('display-element:table.configurator.table.variants.info') },
|
||||
{ value: 'danger', text: this.$t('display-element:table.configurator.table.variants.danger') },
|
||||
{ value: 'warning', text: this.$t('display-element:table.configurator.table.variants.warning') },
|
||||
{ value: 'success', text: this.$t('display-element:table.configurator.table.variants.success') },
|
||||
{ value: 'light', text: this.$t('display-element:table.configurator.table.variants.light') },
|
||||
{ value: 'dark', text: this.$t('display-element:table.configurator.table.variants.dark') },
|
||||
]
|
||||
},
|
||||
|
||||
currentColumns () {
|
||||
if (this.currentConfigurableDatasourceName && this.columns) {
|
||||
const datasourceIndex = this.options.datasources.findIndex(ds => ds.name === this.currentConfigurableDatasourceName)
|
||||
if (datasourceIndex >= 0) {
|
||||
return this.columns[datasourceIndex] || []
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
|
||||
currentSelectedColumns: {
|
||||
get () {
|
||||
return this.currentConfigurableDatasourceName ? this.options.columns[this.currentConfigurableDatasourceName] : []
|
||||
},
|
||||
|
||||
set (columns) {
|
||||
if (this.currentConfigurableDatasourceName) {
|
||||
this.$set(this.options.columns, this.currentConfigurableDatasourceName, columns || [])
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'options.datasources': {
|
||||
immediate: true,
|
||||
handler (datasources) {
|
||||
datasources.forEach(({ name }) => {
|
||||
this.$set(this.options.columns, name, this.options.columns[name] || [])
|
||||
})
|
||||
|
||||
this.currentConfigurableDatasourceName = (datasources[0] || {}).name
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSelectedColumns (columns) {
|
||||
this.currentSelectedColumns = columns.map((c) => {
|
||||
return this.currentColumns.find(({ name }) => name === c)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<b-form-group>
|
||||
<c-rich-text-input
|
||||
v-if="options"
|
||||
v-model="options.value"
|
||||
:labels="{
|
||||
urlPlaceholder: $t('label.urlPlaceholder'),
|
||||
ok: $t('label.ok'),
|
||||
}"
|
||||
/>
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CRichTextInput } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'general',
|
||||
},
|
||||
|
||||
components: {
|
||||
CRichTextInput,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
displayElementOptions: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
datasource: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
options: {
|
||||
get () {
|
||||
return this.displayElementOptions || undefined
|
||||
},
|
||||
|
||||
set (options = {}) {
|
||||
this.$emit('update:displayElementOptions', options)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="displayElement"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general:label.name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="displayElement.name"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-button
|
||||
v-b-toggle.datasources
|
||||
block
|
||||
:disabled="!usesDatasources"
|
||||
variant="primary"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $t('builder:datasources.label') }}
|
||||
</b-button>
|
||||
|
||||
<b-collapse
|
||||
v-if="usesDatasources"
|
||||
id="datasources"
|
||||
:visible="usesDatasources"
|
||||
accordion
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('builder:datasources.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="options.source"
|
||||
:options="sources"
|
||||
@change="setConfigurableSources"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<div
|
||||
v-if="currentConfigurableDatasourceName"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="hasMultipleConfigurableDatasources"
|
||||
:label="$t('builder:joined-datasource-handling')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="currentConfigurableDatasourceName"
|
||||
:options="options.datasources"
|
||||
text-field="name"
|
||||
value-field="name"
|
||||
@change="configurableDatasourceChanged"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<div
|
||||
v-if="currentConfigurableDatasourceIndex >= 0"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="columns.length"
|
||||
:label="$t('builder:prefilter')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<prefilter
|
||||
:filter.sync="options.datasources[currentConfigurableDatasourceIndex].filter"
|
||||
:columns="columns[currentConfigurableDatasourceIndex]"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="columns.length"
|
||||
:label="$t('builder:presort-order')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-presort
|
||||
v-model="options.datasources[currentConfigurableDatasourceIndex].sort"
|
||||
:fields="columns[currentConfigurableDatasourceIndex]"
|
||||
:labels="{
|
||||
add: $t('general:label.add'),
|
||||
ascending: $t('general:label.ascending'),
|
||||
descending: $t('general:label.descending'),
|
||||
none: $t('general:label.none'),
|
||||
addButton: $t('general:label.add'),
|
||||
}"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="displayElement.kind === 'Table'"
|
||||
:label="$t('builder:limit')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="pagingLimit"
|
||||
type="number"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</div>
|
||||
</b-collapse>
|
||||
|
||||
<b-button
|
||||
v-b-toggle.display
|
||||
block
|
||||
:disabled="!showDisplayElementConfigurator"
|
||||
variant="primary"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $t('builder:element') }}
|
||||
</b-button>
|
||||
|
||||
<b-collapse
|
||||
id="display"
|
||||
:visible="showDisplayElementConfigurator"
|
||||
accordion
|
||||
>
|
||||
<component
|
||||
:is="displayElementConfigurator"
|
||||
:display-element-options.sync="options"
|
||||
:columns="columns"
|
||||
:datasource="currentDatasource"
|
||||
/>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import getDisplayElementConfigurator from './loader'
|
||||
import Prefilter from 'corteza-webapp-reporter/src/components/Common/Prefilter'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputPresort } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Prefilter,
|
||||
CInputPresort,
|
||||
},
|
||||
|
||||
props: {
|
||||
displayElement: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
block: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
datasources: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
columns: [],
|
||||
|
||||
currentConfigurableDatasourceName: undefined,
|
||||
currentConfigurableDatasourceIndex: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
usesDatasources () {
|
||||
return !['Text'].includes(this.displayElement.kind)
|
||||
},
|
||||
|
||||
displayElementConfigurator () {
|
||||
return getDisplayElementConfigurator(this.displayElement.kind)
|
||||
},
|
||||
|
||||
showDisplayElementConfigurator () {
|
||||
return this.usesDatasources ? !!this.currentDatasource : true
|
||||
},
|
||||
|
||||
sources () {
|
||||
const options = [{ value: '', text: this.$t('general:label.none') }]
|
||||
|
||||
this.datasources.forEach(({ step }, index) => {
|
||||
Object.values(step).forEach(({ name }) => {
|
||||
options.push({ value: name || `${index}`, text: name || `${index}` })
|
||||
})
|
||||
})
|
||||
|
||||
return options
|
||||
},
|
||||
|
||||
currentDatasource () {
|
||||
if (this.options.source) {
|
||||
return this.datasources.find(({ step: { load = {}, link = {}, join = {}, aggregate = {} } }) => [load.name, link.name, join.name, aggregate.name].includes(this.options.source))
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
|
||||
hasMultipleConfigurableDatasources () {
|
||||
return this.currentDatasource && this.currentDatasource.step.link && this.options.datasources.length > 1
|
||||
},
|
||||
|
||||
options: {
|
||||
get () {
|
||||
return this.displayElement ? this.displayElement.options : undefined
|
||||
},
|
||||
|
||||
set (options = {}) {
|
||||
if (this.displayElement) {
|
||||
this.$emit('update:displayElement', { ...this.displayElement, options })
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
pagingLimit: {
|
||||
get () {
|
||||
let limit = 0
|
||||
|
||||
if (this.currentConfigurableDatasourceIndex >= 0) {
|
||||
const { paging = {} } = this.options.datasources[this.currentConfigurableDatasourceIndex] || {}
|
||||
limit = paging.limit || 0
|
||||
}
|
||||
|
||||
return limit
|
||||
},
|
||||
|
||||
set (limit = 0) {
|
||||
if (this.currentConfigurableDatasourceIndex >= 0) {
|
||||
if (!this.options.datasources[this.currentConfigurableDatasourceIndex].paging) {
|
||||
this.options.datasources[this.currentConfigurableDatasourceIndex].paging = {}
|
||||
}
|
||||
|
||||
this.options.datasources[this.currentConfigurableDatasourceIndex].paging.limit = limit || 0
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'options.source': {
|
||||
immediate: true,
|
||||
handler (source) {
|
||||
this.describeReport(source)
|
||||
},
|
||||
},
|
||||
|
||||
'displayElement.elementID': {
|
||||
immediate: true,
|
||||
handler () {
|
||||
this.currentConfigurableDatasourceIndex = this.datasources.length ? 0 : -1
|
||||
if (this.usesDatasources) {
|
||||
this.currentConfigurableDatasourceName = (this.options.datasources[0] || {}).name
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
describeReport (source) {
|
||||
this.columns = []
|
||||
|
||||
if (source) {
|
||||
const steps = this.datasources.map(({ step }) => step)
|
||||
this.$SystemAPI.reportDescribe({ steps, describe: [this.options.source] })
|
||||
.then((frames = []) => {
|
||||
this.columns = frames.filter(({ source }) => source === this.options.source).map(({ columns = [] }) => columns) || []
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:datasource.describe-failed'))(e)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
setConfigurableSources (source) {
|
||||
this.options.datasources = []
|
||||
this.currentConfigurableDatasourceName = undefined
|
||||
|
||||
let configurableDatasources = []
|
||||
if (source) {
|
||||
configurableDatasources = [source]
|
||||
|
||||
const { link } = this.currentDatasource.step
|
||||
if (link) {
|
||||
configurableDatasources = [link.localSource, link.foreignSource]
|
||||
}
|
||||
}
|
||||
|
||||
this.currentConfigurableDatasourceName = configurableDatasources[0]
|
||||
this.options.datasources = configurableDatasources.map(s => {
|
||||
return {
|
||||
name: s,
|
||||
sort: '',
|
||||
filter: {},
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
configurableDatasourceChanged (source) {
|
||||
if (source) {
|
||||
this.currentConfigurableDatasourceIndex = this.options.datasources.findIndex(({ name }) => source === name)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
import Text from './Text'
|
||||
import Chart from './Chart'
|
||||
import Table from './Table'
|
||||
import Metric from './Metric'
|
||||
|
||||
const Registry = {
|
||||
Text,
|
||||
Chart,
|
||||
Table,
|
||||
Metric,
|
||||
}
|
||||
|
||||
export default function (k) {
|
||||
return Registry[k]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Vue from 'vue'
|
||||
import { capitalize } from 'lodash'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const {
|
||||
CReportChart,
|
||||
CReportMetric,
|
||||
CReportTable,
|
||||
CReportText,
|
||||
} = components
|
||||
|
||||
/**
|
||||
* List of all known display element components
|
||||
*
|
||||
*/
|
||||
const Registry = {
|
||||
Chart: CReportChart,
|
||||
Metric: CReportMetric,
|
||||
Table: CReportTable,
|
||||
Text: CReportText,
|
||||
}
|
||||
|
||||
function GetComponent ({ displayElement }) {
|
||||
if (!displayElement) {
|
||||
throw new Error('displayElement prop missing')
|
||||
}
|
||||
|
||||
const { kind } = displayElement
|
||||
if (Object.hasOwnProperty.call(Registry, capitalize(kind))) {
|
||||
return Registry[kind]
|
||||
}
|
||||
|
||||
throw new Error('unknown displayElement kind: ' + kind)
|
||||
}
|
||||
|
||||
export default Vue.component('display-element', {
|
||||
functional: true,
|
||||
|
||||
render (ce, ctx) {
|
||||
return ce(GetComponent(ctx.props), ctx.data, ctx.children)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="h-100 p-2 overflow-hidden">
|
||||
<b-card
|
||||
no-body
|
||||
class="h-100 border-0 shadow-sm overflow-hidden"
|
||||
>
|
||||
<b-card-header class="sticky-top border-0 p-0">
|
||||
<slot
|
||||
name="header"
|
||||
/>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body
|
||||
v-if="bodySet"
|
||||
class="p-0 h-100"
|
||||
>
|
||||
<slot
|
||||
name="default"
|
||||
/>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
bodySet () {
|
||||
return !!this.$scopedSlots.default
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
import Vue from 'vue'
|
||||
import { capitalize } from 'lodash'
|
||||
|
||||
import Card from './Card'
|
||||
|
||||
const Registry = {
|
||||
Card,
|
||||
}
|
||||
|
||||
const defaultWrap = 'Card'
|
||||
|
||||
/**
|
||||
* @param block {reporter.Block}
|
||||
* @returns component
|
||||
*/
|
||||
function GetWrapComponent ({ block, wrap = defaultWrap }) {
|
||||
const cmpName = capitalize(wrap)
|
||||
if (Object.hasOwnProperty.call(Registry, cmpName)) {
|
||||
return Registry[capitalize(cmpName)]
|
||||
}
|
||||
|
||||
throw new Error('unknown wrap: ' + wrap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps page block with one of the configured (on page block options) components
|
||||
*/
|
||||
export default Vue.component('block', {
|
||||
functional: true,
|
||||
|
||||
render (ce, ctx) {
|
||||
return ce(GetWrapComponent(ctx.props), ctx.data, ctx.children)
|
||||
},
|
||||
})
|
||||
257
client/web/reporter/src/components/Report/Blocks/index.vue
Normal file
257
client/web/reporter/src/components/Report/Blocks/index.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<wrap
|
||||
v-if="block"
|
||||
v-bind="$props"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template
|
||||
#header
|
||||
>
|
||||
<div
|
||||
v-if="block.title || block.description"
|
||||
class="d-flex flex-column p-3 border-bottom gap-1"
|
||||
>
|
||||
<h4
|
||||
v-if="block.title"
|
||||
class="text-primary text-truncate mb-0"
|
||||
>
|
||||
{{ block.title }}
|
||||
</h4>
|
||||
|
||||
<b-card-text
|
||||
v-if="block.description"
|
||||
class="text-dark text-wrap"
|
||||
>
|
||||
{{ block.description }}
|
||||
</b-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<split
|
||||
v-if="showDisplayElements"
|
||||
ref="split"
|
||||
:direction="block.layout"
|
||||
:gutter-size="12"
|
||||
class="h-100"
|
||||
@onDragEnd="setDisplayElementSizes"
|
||||
>
|
||||
<split-area
|
||||
v-for="(element, displayElementIndex) in block.elements"
|
||||
:key="displayElementIndex"
|
||||
:size="element.meta.size"
|
||||
:min-size="0"
|
||||
:class="{
|
||||
'overflow-hidden h-100': element.kind !== 'Text',
|
||||
'w-100': block.elements.length === 1,
|
||||
}"
|
||||
class="position-relative"
|
||||
>
|
||||
<div
|
||||
v-if="processing"
|
||||
class="d-flex align-items-center justify-content-center h-100"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<display-element
|
||||
v-else
|
||||
:display-element="element"
|
||||
:labels="{
|
||||
previous: $t('display-element:table.view.previous'),
|
||||
next: $t('display-element:table.view.next'),
|
||||
}"
|
||||
@update="updateDataframes({ displayElementIndex, definition: $event })"
|
||||
/>
|
||||
</split-area>
|
||||
</split>
|
||||
</template>
|
||||
</wrap>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Wrap from './Wrap'
|
||||
import { Split, SplitArea } from 'vue-split-panel'
|
||||
import DisplayElement from './DisplayElements/Viewers'
|
||||
import { reporter } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
name: 'Block',
|
||||
|
||||
components: {
|
||||
Split,
|
||||
SplitArea,
|
||||
Wrap,
|
||||
DisplayElement,
|
||||
},
|
||||
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
default: () => -1,
|
||||
},
|
||||
|
||||
block: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
scenario: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
reportID: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '0',
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
|
||||
dataframes: {},
|
||||
|
||||
showDisplayElements: false,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'block.blockID': {
|
||||
immediate: true,
|
||||
handler () {
|
||||
this.renderBlock()
|
||||
},
|
||||
},
|
||||
|
||||
'block.elements.length': {
|
||||
handler (length, oldLength) {
|
||||
const addedOrRemoved = length !== oldLength && oldLength
|
||||
|
||||
if (addedOrRemoved) {
|
||||
const defaultSize = Math.floor(100 / length)
|
||||
|
||||
// Reset sizes to default if element was added or removed
|
||||
this.block.elements = this.block.elements.map(e => {
|
||||
e.meta.size = defaultSize
|
||||
return e
|
||||
})
|
||||
}
|
||||
|
||||
this.renderBlock()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
renderBlock () {
|
||||
const { elements = [] } = this.block || {}
|
||||
if (elements.length) {
|
||||
this.runReport()
|
||||
|
||||
// Hack around split not rerendering
|
||||
this.showDisplayElements = false
|
||||
this.$nextTick().then(() => {
|
||||
this.showDisplayElements = true
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
setDisplayElementSizes (sizes = []) {
|
||||
sizes.forEach((size, index) => {
|
||||
this.block.elements[index].meta.size = size
|
||||
})
|
||||
|
||||
this.$emit('item-updated', this.index)
|
||||
},
|
||||
|
||||
getScenarioDefinition (element) {
|
||||
const scenarioDefinition = {}
|
||||
|
||||
// Generate filter for each load datasource
|
||||
if (this.scenario.filters) {
|
||||
element.options.datasources.forEach(({ name }) => {
|
||||
scenarioDefinition[name] = {
|
||||
ref: name,
|
||||
filter: this.scenario.filters[name] || {},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return scenarioDefinition
|
||||
},
|
||||
|
||||
runReport () {
|
||||
this.processing = true
|
||||
this.dataframes = {}
|
||||
const frames = []
|
||||
|
||||
this.block.elements.forEach((element, key) => {
|
||||
element = reporter.DisplayElementMaker(element)
|
||||
|
||||
if (element && element.kind !== 'Text') {
|
||||
if (element.elementID === '0') {
|
||||
element.elementID = `${key}`
|
||||
}
|
||||
|
||||
const { dataframes = [] } = element.reportDefinitions(this.getScenarioDefinition(element))
|
||||
|
||||
frames.push(...dataframes.filter(({ source }) => source))
|
||||
}
|
||||
})
|
||||
|
||||
if (frames.length) {
|
||||
this.$SystemAPI.reportRun({ frames, reportID: this.reportID })
|
||||
.then(({ frames = [] }) => {
|
||||
this.block.elements = this.block.elements.map((element, key) => {
|
||||
if (element.elementID === '0') {
|
||||
element.elementID = `${key}`
|
||||
}
|
||||
const dataframes = frames.filter(({ name }) => name === element.elementID)
|
||||
return { ...element, dataframes }
|
||||
})
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:report.runFailed'))(e)
|
||||
}).finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
} else {
|
||||
this.processing = false
|
||||
}
|
||||
},
|
||||
|
||||
updateDataframes ({ displayElementIndex, definition }) {
|
||||
const element = reporter.DisplayElementMaker(this.block.elements[displayElementIndex])
|
||||
const frames = []
|
||||
|
||||
if (element && element.kind !== 'Text') {
|
||||
const scenarioDefinition = this.getScenarioDefinition(element)
|
||||
Object.entries(definition).forEach(([key, value]) => {
|
||||
definition[key] = { ...value, ...scenarioDefinition[key] }
|
||||
})
|
||||
|
||||
const { dataframes = [] } = element.reportDefinitions(definition)
|
||||
|
||||
frames.push(...dataframes.filter(({ source }) => source))
|
||||
|
||||
if (frames.length) {
|
||||
this.$SystemAPI.reportRun({ frames, reportID: this.reportID })
|
||||
.then(({ frames = [] }) => {
|
||||
this.block.elements.find(({ elementID }) => elementID === element.elementID).dataframes = frames
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:report.runFailed'))(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.split .gutter {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
44
client/web/reporter/src/components/Report/C3.js
Normal file
44
client/web/reporter/src/components/Report/C3.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// eslint-disable-next-line
|
||||
import { default as component } from './Grid.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
editable: true,
|
||||
blocks: [
|
||||
{
|
||||
description: 'Description',
|
||||
title: 'Title',
|
||||
x: 0,
|
||||
w: 20,
|
||||
y: 0,
|
||||
h: 15,
|
||||
i: 0,
|
||||
xywh: [0, 0, 20, 15],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Grid',
|
||||
group: ['Common'],
|
||||
component,
|
||||
props,
|
||||
controls: [
|
||||
checkbox('Editable', 'editable'),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
editable: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<c-form-table-wrapper
|
||||
:labels="{
|
||||
addButton: $t('general:label.add')
|
||||
}"
|
||||
@add-item="addColumn"
|
||||
>
|
||||
<b-table-simple
|
||||
v-if="columns.length"
|
||||
responsive
|
||||
borderless
|
||||
small
|
||||
>
|
||||
<b-thead>
|
||||
<b-tr>
|
||||
<b-th
|
||||
class="w-25"
|
||||
>
|
||||
{{ $t('datasources:name') }}
|
||||
</b-th>
|
||||
|
||||
<b-th
|
||||
class="w-25"
|
||||
>
|
||||
{{ $t('datasources:label') }}
|
||||
</b-th>
|
||||
|
||||
<b-th
|
||||
class="w-50"
|
||||
>
|
||||
{{ $t('datasources:expression') }}
|
||||
</b-th>
|
||||
</b-tr>
|
||||
</b-thead>
|
||||
|
||||
<b-tbody>
|
||||
<b-tr
|
||||
v-for="(column, index) in columns"
|
||||
:key="index"
|
||||
>
|
||||
<b-td>
|
||||
<b-form-input
|
||||
v-model="column.name"
|
||||
:placeholder="$t('datasources:new.name')"
|
||||
/>
|
||||
</b-td>
|
||||
|
||||
<b-td>
|
||||
<b-form-input
|
||||
v-model="column.label"
|
||||
:placeholder="$t('datasources:new.label')"
|
||||
/>
|
||||
</b-td>
|
||||
|
||||
<b-td>
|
||||
<b-form-input
|
||||
v-model="column.def.raw"
|
||||
:placeholder="$t('datasources:expression')"
|
||||
/>
|
||||
</b-td>
|
||||
|
||||
<b-td class="align-middle">
|
||||
<c-input-confirm
|
||||
show-icon
|
||||
@confirmed="deleteColumn(index)"
|
||||
/>
|
||||
</b-td>
|
||||
</b-tr>
|
||||
</b-tbody>
|
||||
</b-table-simple>
|
||||
</c-form-table-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
aggregate: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
columns: {
|
||||
get () {
|
||||
return this.aggregate || []
|
||||
},
|
||||
|
||||
set (aggregate) {
|
||||
this.$emit('update:aggregate', aggregate)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addColumn () {
|
||||
this.columns.push({
|
||||
name: '',
|
||||
label: '',
|
||||
def: {
|
||||
raw: '',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
deleteColumn (index) {
|
||||
this.columns.splice(index, 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<c-form-table-wrapper
|
||||
:labels="{
|
||||
addButton: $t('general:label.add')
|
||||
}"
|
||||
@add-item="addParam"
|
||||
>
|
||||
<b-table-simple
|
||||
v-if="groups.length"
|
||||
responsive
|
||||
borderless
|
||||
small
|
||||
>
|
||||
<b-thead>
|
||||
<b-tr>
|
||||
<b-th
|
||||
class="w-25"
|
||||
>
|
||||
{{ $t('datasources:name') }}
|
||||
</b-th>
|
||||
|
||||
<b-th
|
||||
class="w-25"
|
||||
>
|
||||
{{ $t('datasources:label') }}
|
||||
</b-th>
|
||||
|
||||
<b-th
|
||||
class="w-50"
|
||||
>
|
||||
{{ $t('datasources:expression') }}
|
||||
</b-th>
|
||||
</b-tr>
|
||||
</b-thead>
|
||||
|
||||
<b-tbody>
|
||||
<b-tr
|
||||
v-for="(group, index) in groups"
|
||||
:key="index"
|
||||
>
|
||||
<b-td>
|
||||
<b-form-input
|
||||
v-model="group.name"
|
||||
:placeholder="$t('datasources:new.name')"
|
||||
/>
|
||||
</b-td>
|
||||
|
||||
<b-td>
|
||||
<b-form-input
|
||||
v-model="group.label"
|
||||
:placeholder="$t('datasources:new.label')"
|
||||
/>
|
||||
</b-td>
|
||||
|
||||
<b-td>
|
||||
<b-form-input
|
||||
v-model="group.def.raw"
|
||||
:placeholder="$t('datasources:expression')"
|
||||
/>
|
||||
</b-td>
|
||||
|
||||
<b-td class="align-middle">
|
||||
<c-input-confirm
|
||||
show-icon
|
||||
@confirmed="deleteGroup(index)"
|
||||
/>
|
||||
</b-td>
|
||||
</b-tr>
|
||||
</b-tbody>
|
||||
</b-table-simple>
|
||||
</c-form-table-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
groupBy: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
groups: {
|
||||
get () {
|
||||
return this.groupBy || []
|
||||
},
|
||||
|
||||
set (groupBy) {
|
||||
this.$emit('update:groupBy', groupBy)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addParam () {
|
||||
this.groups.push({
|
||||
name: '',
|
||||
label: '',
|
||||
def: {
|
||||
raw: '',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
deleteGroup (index) {
|
||||
this.groups.splice(index, 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table td.fit,
|
||||
.table th.fit {
|
||||
white-space: nowrap;
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
.btn-add-group {
|
||||
&:hover, &:active {
|
||||
background-color: var(--primary) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-border {
|
||||
background-image: linear-gradient(to left, lightgray, lightgray);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 1px;
|
||||
background-position: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="step.aggregate"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('datasources:name-required')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="step.aggregate.name"
|
||||
:state="nameState"
|
||||
:disabled="!creating"
|
||||
:placeholder="$t('datasources:datasource-name')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<hr>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('datasources:source')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="step.aggregate.source"
|
||||
:options="supportedSources"
|
||||
@change="reset"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
:value="undefined"
|
||||
>
|
||||
{{ $t('general:label.none') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
|
||||
<div
|
||||
v-if="step.aggregate.source"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('datasources:group-by')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<group-by
|
||||
:group-by.sync="step.aggregate.keys"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('datasources:aggregate')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<aggregate
|
||||
:aggregate.sync="step.aggregate.columns"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<!-- <b-form-group
|
||||
v-if="columns.length"
|
||||
label="Prefilter"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<prefilter
|
||||
:filter.sync="step.aggregate.filter"
|
||||
:columns="columns"
|
||||
/>
|
||||
</b-form-group> -->
|
||||
|
||||
<!-- <b-form-group
|
||||
v-if="columns.length"
|
||||
label="Presort order"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-presort
|
||||
v-model="step.aggregate.sort"
|
||||
:fields="columns"
|
||||
:labels="{
|
||||
add: $t('general:label.add'),
|
||||
ascending: $t('general:label.ascending'),
|
||||
descending: $t('general:label.descending'),
|
||||
none: $t('general:label.none'),
|
||||
}"
|
||||
/>
|
||||
</b-form-group> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from '../base.vue'
|
||||
import GroupBy from './GroupBy'
|
||||
import Aggregate from './Aggregate'
|
||||
// import Prefilter from 'corteza-webapp-reporter/src/components/Common/Prefilter'
|
||||
// import { components } from '@cortezaproject/corteza-vue'
|
||||
// const { CInputPresort } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GroupBy,
|
||||
Aggregate,
|
||||
// Prefilter,
|
||||
// CInputPresort,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
props: {
|
||||
datasources: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
columns: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
supportedSources () {
|
||||
const options = []
|
||||
|
||||
this.datasources.forEach(({ step }, index) => {
|
||||
Object.entries(step).forEach(([kind, { name }]) => {
|
||||
if (kind === 'load') {
|
||||
options.push({ value: name || `${index}`, text: name || `${index}` })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return options
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getSourceColumns () {
|
||||
const steps = this.datasources.filter(({ step }) => step.load).map(({ step }) => step)
|
||||
steps.push(this.step)
|
||||
const describe = [this.step.aggregate.name]
|
||||
|
||||
if (steps.length && describe.length) {
|
||||
this.$SystemAPI.reportDescribe({ steps, describe })
|
||||
.then((frames = []) => {
|
||||
const { columns = [] } = frames.find(({ source }) => describe.includes(source)) || {}
|
||||
this.columns = columns
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:datasource.describe-failed'))(e)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.step.aggregate.filter = {}
|
||||
this.step.aggregate.sort = ''
|
||||
this.step.aggregate.keys = []
|
||||
this.step.aggregate.columns = []
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
197
client/web/reporter/src/components/Report/Datasources/Join.vue
Normal file
197
client/web/reporter/src/components/Report/Datasources/Join.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="step.join"
|
||||
>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('datasources:name-required')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="step.join.name"
|
||||
:disabled="!creating"
|
||||
:state="nameState"
|
||||
:placeholder="$t('datasources:datasource-name')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('datasources:primary.source')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="step.join.localSource"
|
||||
:options="supportedSources"
|
||||
@change="onSourceChange('local')"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
value=""
|
||||
>
|
||||
{{ $t('general:label.none') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('datasources:secondary.source')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="step.join.foreignSource"
|
||||
:options="supportedSources"
|
||||
@change="onSourceChange('foreign')"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
value=""
|
||||
>
|
||||
{{ $t('general:label.none') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="step.join.localSource"
|
||||
:label="$t('datasources:primary.column')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<column-selector
|
||||
v-model="step.join.localColumn"
|
||||
:columns="localColumns"
|
||||
style="min-width: 100% !important;"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="step.join.foreignSource"
|
||||
:label="$t('datasources:secondary.column')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<column-selector
|
||||
v-model="step.join.foreignColumn"
|
||||
:columns="foreignColumns"
|
||||
style="min-width: 100% !important;"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base.vue'
|
||||
import ColumnSelector from 'corteza-webapp-reporter/src/components/Common/ColumnSelector.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnSelector,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
localColumns: [],
|
||||
foreignColumns: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
supportedSources () {
|
||||
const options = []
|
||||
|
||||
this.datasources.forEach(({ step }, index) => {
|
||||
Object.entries(step).forEach(([kind, { name }]) => {
|
||||
if (['load', 'group', 'join'].includes(kind) && index !== this.index) {
|
||||
options.push({ value: name || `${index}`, text: name || `${index}` })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return options
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'step.join.name': {
|
||||
immediate: true,
|
||||
handler (newStep, oldStep) {
|
||||
if (!oldStep && newStep) {
|
||||
this.getSourceColumns(['local', 'foreign'])
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
'step.join.localSource': {
|
||||
handler () {
|
||||
this.getSourceColumns(['local'])
|
||||
},
|
||||
},
|
||||
|
||||
'step.join.foreignSource': {
|
||||
handler () {
|
||||
this.getSourceColumns(['foreign'])
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSourceChange (source) {
|
||||
this.step.join[`${source}Column`] = ''
|
||||
},
|
||||
|
||||
async getSourceColumns (sources = []) {
|
||||
sources.forEach(source => {
|
||||
this[`${source}Columns`] = []
|
||||
|
||||
const sourceType = this.step.join[`${source}Source`]
|
||||
|
||||
if (sourceType) {
|
||||
const steps = this.datasources.filter(({ step }, index) => index !== this.index && !step.link).map(({ step }) => step)
|
||||
const describe = [sourceType]
|
||||
|
||||
if (steps.length && describe.length) {
|
||||
this.$SystemAPI.reportDescribe({ steps, describe })
|
||||
.then((frames = []) => {
|
||||
const { columns = [] } = frames.find(({ source }) => describe.includes(source)) || {}
|
||||
this[`${source}Columns`] = columns
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:datasource.describe-failed'))(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
195
client/web/reporter/src/components/Report/Datasources/Link.vue
Normal file
195
client/web/reporter/src/components/Report/Datasources/Link.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="step.link"
|
||||
>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('datasources:name-required')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="step.link.name"
|
||||
:disabled="!creating"
|
||||
:state="nameState"
|
||||
:placeholder="$t('datasources:datasource-name')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('datasources:primary.source')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="step.link.localSource"
|
||||
:options="supportedSources"
|
||||
@change="onSourceChange('local')"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
value=""
|
||||
>
|
||||
{{ $t('general:label.none') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('datasources:secondary.source')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="step.link.foreignSource"
|
||||
:options="supportedSources"
|
||||
@change="onSourceChange('foreign')"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
value=""
|
||||
>
|
||||
{{ $t('general:label.none') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="step.link.localSource"
|
||||
:label="$t('datasources:primary.column')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<column-selector
|
||||
v-model="step.link.localColumn"
|
||||
:columns="localColumns"
|
||||
style="min-width: 100% !important;"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="step.link.foreignSource"
|
||||
:label="$t('datasources:secondary.column')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<column-selector
|
||||
v-model="step.link.foreignColumn"
|
||||
:columns="foreignColumns"
|
||||
style="min-width: 100% !important;"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base.vue'
|
||||
import ColumnSelector from 'corteza-webapp-reporter/src/components/Common/ColumnSelector.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnSelector,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
localColumns: [],
|
||||
foreignColumns: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
supportedSources () {
|
||||
const options = []
|
||||
|
||||
this.datasources.forEach(({ step }, index) => {
|
||||
Object.entries(step).forEach(([kind, { name }]) => {
|
||||
if (['load', 'group'].includes(kind)) {
|
||||
options.push({ value: name || `${index}`, text: name || `${index}` })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return options
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'step.link.name': {
|
||||
immediate: true,
|
||||
handler (newStep, oldStep) {
|
||||
if (!oldStep && newStep) {
|
||||
this.getSourceColumns(['local', 'foreign'])
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
'step.link.localSource': {
|
||||
handler () {
|
||||
this.getSourceColumns(['local'])
|
||||
},
|
||||
},
|
||||
|
||||
'step.link.foreignSource': {
|
||||
handler () {
|
||||
this.getSourceColumns(['foreign'])
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSourceChange (source) {
|
||||
this.step.link[`${source}Column`] = ''
|
||||
},
|
||||
|
||||
async getSourceColumns (sources = []) {
|
||||
sources.forEach(source => {
|
||||
this[`${source}Columns`] = []
|
||||
|
||||
const sourceType = this.step.link[`${source}Source`]
|
||||
|
||||
if (sourceType) {
|
||||
const steps = this.datasources.filter(({ step }) => step.load || step.aggregate).map(({ step }) => step)
|
||||
const describe = [sourceType]
|
||||
|
||||
if (steps.length && describe.length) {
|
||||
this.$SystemAPI.reportDescribe({ steps, describe })
|
||||
.then((frames = []) => {
|
||||
const { columns = [] } = frames.find(({ source }) => describe.includes(source)) || {}
|
||||
this[`${source}Columns`] = columns
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:datasource.describe-failed'))(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('datasources:namespace')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="namespace"
|
||||
:options="namespaces"
|
||||
text-field="name"
|
||||
value-field="namespaceID"
|
||||
@change="selectNamespace"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
:value="undefined"
|
||||
>
|
||||
{{ $t('general:label.none') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
v-if="namespace"
|
||||
:label="$t('datasources:module')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="module"
|
||||
:options="modules"
|
||||
text-field="name"
|
||||
value-field="moduleID"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
:value="undefined"
|
||||
>
|
||||
{{ $t('general:label.none') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
definition: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
|
||||
namespaces: [],
|
||||
modules: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
namespace: {
|
||||
get () {
|
||||
return this.definition.namespaceID
|
||||
},
|
||||
|
||||
set (namespaceID) {
|
||||
this.$emit('update:definition', { ...this.definition, namespaceID })
|
||||
},
|
||||
},
|
||||
|
||||
module: {
|
||||
get () {
|
||||
return this.definition.moduleID
|
||||
},
|
||||
|
||||
set (moduleID) {
|
||||
this.$emit('update:definition', { ...this.definition, moduleID })
|
||||
},
|
||||
},
|
||||
|
||||
filter: {
|
||||
get () {
|
||||
return this.definition.filter
|
||||
},
|
||||
|
||||
set (filter) {
|
||||
this.$emit('update:definition', { ...this.definition, filter })
|
||||
},
|
||||
},
|
||||
|
||||
sort: {
|
||||
get () {
|
||||
return this.definition.sort
|
||||
},
|
||||
|
||||
set (sort) {
|
||||
this.$emit('update:definition', { ...this.definition, sort })
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
namespace: {
|
||||
immediate: true,
|
||||
handler (namespace) {
|
||||
if (namespace) {
|
||||
this.processing = true
|
||||
|
||||
this.fetchModules(namespace)
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.processing = true
|
||||
|
||||
this.fetchNamespaces()
|
||||
.then(() => {
|
||||
if (this.namespace) {
|
||||
return this.fetchModules(this.namespace)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectNamespace () {
|
||||
this.module = undefined
|
||||
},
|
||||
|
||||
fetchNamespaces () {
|
||||
return this.$ComposeAPI.namespaceList({ sort: 'name' }).then(({ set = [] }) => {
|
||||
this.namespaces = set
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:namespace.fetch-failed'))(e)
|
||||
})
|
||||
},
|
||||
|
||||
fetchModules (namespaceID) {
|
||||
return this.$ComposeAPI.moduleList({ namespaceID, sort: 'name' }).then(({ set = [] }) => {
|
||||
this.modules = set
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:module.fetch-failed'))(e)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="step.load"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('datasources:name-required')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="step.load.name"
|
||||
:state="nameState"
|
||||
:disabled="!creating"
|
||||
:placeholder="$t('datasources:datasource-name')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<hr>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('datasources:source')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="step.load.source"
|
||||
:options="supportedSources"
|
||||
@change="reset"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<component
|
||||
:is="sourceTypeComponent(step.load.source)"
|
||||
v-if="step.load.source"
|
||||
:definition.sync="stepDefinition"
|
||||
/>
|
||||
|
||||
<b-form-group
|
||||
v-if="columns.length"
|
||||
:label="$t('datasources:prefilter')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<prefilter
|
||||
:filter.sync="step.load.filter"
|
||||
:columns="columns"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<!-- <b-form-group
|
||||
v-if="columns.length"
|
||||
label="Presort order"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-presort
|
||||
v-model="step.load.sort"
|
||||
:fields="columns"
|
||||
:labels="{
|
||||
add: $t('general:label.add'),
|
||||
ascending: $t('general:label.ascending'),
|
||||
descending: $t('general:label.descending'),
|
||||
none: $t('general:label.none'),
|
||||
}"
|
||||
/>
|
||||
</b-form-group> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from '../base.vue'
|
||||
import loader from './loader'
|
||||
import Prefilter from 'corteza-webapp-reporter/src/components/Common/Prefilter'
|
||||
// import { components } from '@cortezaproject/corteza-vue'
|
||||
// const { CInputPresort } = components
|
||||
export default {
|
||||
components: {
|
||||
Prefilter,
|
||||
// CInputPresort,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
// @todo get this from the API
|
||||
supportedSources: [
|
||||
{
|
||||
text: this.$t('datasources:compose-records'),
|
||||
value: 'composeRecords',
|
||||
definition: [{
|
||||
label: 'namespace',
|
||||
key: 'namespace',
|
||||
}, {
|
||||
label: 'module',
|
||||
key: 'module',
|
||||
}],
|
||||
},
|
||||
],
|
||||
|
||||
columns: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
stepDefinition: {
|
||||
get () {
|
||||
return this.step.load ? this.step.load.definition : undefined
|
||||
},
|
||||
|
||||
set (definition) {
|
||||
this.step.load.definition = definition
|
||||
this.$emit('update:step', this.step)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
stepDefinition: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler ({ moduleID, namespaceID }) {
|
||||
if (moduleID && namespaceID) {
|
||||
this.getSourceColumns()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
sourceTypeComponent: loader,
|
||||
|
||||
async getSourceColumns () {
|
||||
const steps = [this.step]
|
||||
const describe = [this.step.load.name]
|
||||
|
||||
if (steps.length && describe.length) {
|
||||
this.$SystemAPI.reportDescribe({ steps, describe })
|
||||
.then((frames = []) => {
|
||||
const { columns = [] } = frames.find(({ source }) => describe.includes(source)) || {}
|
||||
this.columns = columns
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:datasource.describe-failed'))(e)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.step.load.filter = {}
|
||||
this.step.load.sort = ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
import ComposeRecordConfigurator from './ComposeRecordConfigurator'
|
||||
|
||||
const Registry = {
|
||||
composeRecords: ComposeRecordConfigurator,
|
||||
}
|
||||
|
||||
export default function (k) {
|
||||
return Registry[k]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
|
||||
step: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
datasources: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
creating: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
kind () {
|
||||
return Object.keys(this.step)
|
||||
},
|
||||
|
||||
nameState () {
|
||||
const name = this.step[this.kind].name
|
||||
const isDuplicate = this.datasources.some(({ step }, index) => {
|
||||
return index !== this.index && step[Object.keys(step)].name === name && name
|
||||
})
|
||||
|
||||
return name.length > 0 && !isDuplicate ? null : false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { default as Load } from './Load'
|
||||
import { default as Link } from './Link'
|
||||
import { default as Join } from './Join'
|
||||
import { default as Aggregate } from './Aggregate'
|
||||
|
||||
const Registry = {
|
||||
load: Load,
|
||||
link: Link,
|
||||
join: Join,
|
||||
aggregate: Aggregate,
|
||||
}
|
||||
|
||||
export default function (k) {
|
||||
return Registry[k]
|
||||
}
|
||||
124
client/web/reporter/src/components/Report/Grid.vue
Normal file
124
client/web/reporter/src/components/Report/Grid.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="layout.length"
|
||||
class="w-100"
|
||||
>
|
||||
<grid-layout
|
||||
:layout.sync="layout"
|
||||
:col-num="48"
|
||||
:row-height="10"
|
||||
vertical-compact
|
||||
:is-resizable="editable"
|
||||
:is-draggable="editable"
|
||||
:cols="{ lg: 48, md: 48, sm: 1, xs: 1, xxs: 1 }"
|
||||
:margin="[0, 0]"
|
||||
:responsive="!editable"
|
||||
:use-css-transforms="false"
|
||||
>
|
||||
<grid-item
|
||||
v-for="(item, index) in grid"
|
||||
:key="item.i"
|
||||
ref="items"
|
||||
:min-w="3"
|
||||
:min-h="3"
|
||||
:i="item.i"
|
||||
:h="item.h"
|
||||
:w="item.w"
|
||||
:x="item.x"
|
||||
:y="item.y"
|
||||
:class="{ 'editable-grid-item': editable }"
|
||||
drag-ignore-from=".gutter"
|
||||
@moved="onBlockUpdated(index)"
|
||||
@resized="onBlockUpdated(index)"
|
||||
>
|
||||
<slot
|
||||
:block="blocks[item.i]"
|
||||
:index="index"
|
||||
:block-index="item.i"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</grid-item>
|
||||
</grid-layout>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="d-flex align-items-center justify-content-center h-50 w-100"
|
||||
>
|
||||
<h4>
|
||||
{{ $t('builder:no-blocks-added') }}
|
||||
</h4>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GridLayout, GridItem } from 'vue-grid-layout'
|
||||
|
||||
export default {
|
||||
name: 'Grid',
|
||||
|
||||
components: {
|
||||
GridLayout,
|
||||
GridItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
blocks: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
// All blocks in vue-grid friendly structure
|
||||
grid: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
layout: {
|
||||
get () {
|
||||
return this.grid || this.blocks
|
||||
},
|
||||
|
||||
set (layout) {
|
||||
// Only update parent blocks when editable to avoid unnecessary updates
|
||||
if (this.editable) {
|
||||
this.$emit('update:blocks', layout)
|
||||
} else {
|
||||
this.grid = layout
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
blocks: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (blocks) {
|
||||
if (this.editable) {
|
||||
this.grid = blocks
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onBlockUpdated (index) {
|
||||
this.$emit('item-updated', index)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vue-grid-item.vue-grid-placeholder {
|
||||
background: var(--primary) !important;
|
||||
}
|
||||
</style>
|
||||
133
client/web/reporter/src/components/Report/Scenarios/index.vue
Normal file
133
client/web/reporter/src/components/Report/Scenarios/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="scenario"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('scenarios:label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="scenario.label"
|
||||
:placeholder="$t('scenarios:scenario-name')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('scenarios:datasource')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="currentDatasourceName"
|
||||
:options="datasourceOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="currentDatasourceName && scenario.filters[currentDatasourceName]"
|
||||
:label="$t('scenarios:prefilter')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<prefilter
|
||||
:filter="scenario.filters[currentDatasourceName]"
|
||||
:columns="columns"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Prefilter from 'corteza-webapp-reporter/src/components/Common/Prefilter'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Prefilter,
|
||||
},
|
||||
|
||||
props: {
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: () => -1,
|
||||
},
|
||||
|
||||
scenario: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
datasources: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
currentDatasourceName: '',
|
||||
columns: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
datasourceOptions () {
|
||||
const options = [{ value: '', text: this.$t('general:label.none') }]
|
||||
|
||||
this.datasources.forEach(({ step }, index) => {
|
||||
Object.entries(step).forEach(([kind, { name }]) => {
|
||||
if (['load'].includes(kind)) {
|
||||
options.push({ value: name || `${index}`, text: name || `${index}` })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return options
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
currentIndex: {
|
||||
immediate: true,
|
||||
handler () {
|
||||
// Select first defined filter on switch
|
||||
const { filters = {} } = this.scenario
|
||||
const definedFilters = Object.keys(filters)
|
||||
|
||||
this.currentDatasourceName = definedFilters.length ? definedFilters[0] : ''
|
||||
},
|
||||
},
|
||||
|
||||
currentDatasourceName: {
|
||||
immediate: true,
|
||||
handler (name) {
|
||||
if (name && !this.scenario.filters[name]) {
|
||||
this.scenario.filters[name] = {}
|
||||
}
|
||||
|
||||
this.getSourceColumns()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getSourceColumns () {
|
||||
this.columns = []
|
||||
|
||||
if (this.currentDatasourceName) {
|
||||
const steps = this.datasources.map(({ step }) => step)
|
||||
|
||||
this.$SystemAPI.reportDescribe({ steps, describe: [this.currentDatasourceName] })
|
||||
.then((frames = []) => {
|
||||
this.columns = ((frames[0] || {}).columns || [])
|
||||
}).catch((e) => {
|
||||
this.toastErrorHandler(this.$t('notification:datasource.describe-failed'))(e)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
124
client/web/reporter/src/components/ReportSidebar.vue
Normal file
124
client/web/reporter/src/components/ReportSidebar.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div>
|
||||
<portal to="sidebar-body-expanded">
|
||||
<div
|
||||
v-if="reports.length"
|
||||
class="h-100"
|
||||
>
|
||||
<div class="bg-white sticky-top py-2">
|
||||
<b-button
|
||||
data-test-id="button-report-list"
|
||||
variant="light"
|
||||
class="w-100 mb-2"
|
||||
:to="{ name: 'report.list' }"
|
||||
>
|
||||
{{ $t('report-list') }}
|
||||
</b-button>
|
||||
<c-input-search
|
||||
v-model.trim="query"
|
||||
:placeholder="$t('search-reports')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<c-sidebar-nav-items
|
||||
:items="filteredReports"
|
||||
:start-expanded="!!query"
|
||||
default-route-name="report.view"
|
||||
class="overflow-auto h-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h5
|
||||
v-else
|
||||
class="d-flex justify-content-center mt-5"
|
||||
>
|
||||
{{ $t('no-reports') }}
|
||||
</h5>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CSidebarNavItems, CInputSearch } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'sidebar',
|
||||
},
|
||||
|
||||
components: {
|
||||
CSidebarNavItems,
|
||||
CInputSearch,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
query: '',
|
||||
|
||||
reports: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredReports () {
|
||||
let reports = this.reports
|
||||
if (this.query) {
|
||||
reports = this.reports.filter(({ reportID, handle, meta: { name = '' } }) => {
|
||||
const reportString = `${reportID}${handle}${name}`.toLowerCase().trim()
|
||||
return reportString.indexOf(this.query.toLowerCase().trim()) > -1
|
||||
})
|
||||
}
|
||||
|
||||
return reports.map(({ reportID, handle, meta: { name = '' } }) => {
|
||||
return {
|
||||
page: { pageID: reportID, name: this.$route.name, title: name || handle },
|
||||
params: { reportID },
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.name': {
|
||||
immediate: true,
|
||||
handler ({ name }) {
|
||||
if (!['report.list', 'report.create', 'report.edit'].includes(name)) {
|
||||
this.fetchReports()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$root.$on('refetch:reports', this.fetchReports)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.$root.$off('refetch:reports', this.fetchReports)
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchReports () {
|
||||
this.$SystemAPI.reportList()
|
||||
.then(res => {
|
||||
this.reports = (res || {}).set || []
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:report.listFetchFailed')))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// This has to be there, so chevrons are clickable inside the button
|
||||
.pointer-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Using font-weight-bold moves the sidebar nav content; text-stroke keeps in nicely in place
|
||||
.nav-active {
|
||||
color: var(--primary);
|
||||
-webkit-text-stroke: 0.4px var(--primary);
|
||||
}
|
||||
</style>
|
||||
133
client/web/reporter/src/components/faIcons.js
Normal file
133
client/web/reporter/src/components/faIcons.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCheck,
|
||||
faCheckDouble,
|
||||
faTimes,
|
||||
faHome,
|
||||
faSave,
|
||||
faKeyboard,
|
||||
faMouse,
|
||||
faCog,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronDown,
|
||||
faGripHorizontal,
|
||||
faUserCog,
|
||||
faExclamationTriangle,
|
||||
faSearchPlus,
|
||||
faSearchMinus,
|
||||
faSearch,
|
||||
faPlus,
|
||||
faThumbtack,
|
||||
faPen,
|
||||
faCogs,
|
||||
faRemoveFormat,
|
||||
faBold,
|
||||
faItalic,
|
||||
faUnderline,
|
||||
faStrikethrough,
|
||||
faQuoteRight,
|
||||
faCode,
|
||||
faParagraph,
|
||||
faListOl,
|
||||
faListUl,
|
||||
faTasks,
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
faAlignCenter,
|
||||
faAlignJustify,
|
||||
faAngleRight,
|
||||
faAngleLeft,
|
||||
faAngleDoubleLeft,
|
||||
faLink,
|
||||
faBars,
|
||||
faAngleUp,
|
||||
faAngleDown,
|
||||
faFilter,
|
||||
faGripVertical,
|
||||
faEyeSlash,
|
||||
faQuestion,
|
||||
faEllipsisV,
|
||||
faTools,
|
||||
faClone,
|
||||
faDownload,
|
||||
faTable,
|
||||
faBellSlash,
|
||||
faBell as faBellSolid,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
faEye,
|
||||
faEdit,
|
||||
faTrashAlt,
|
||||
faQuestionCircle,
|
||||
faUser,
|
||||
faBell,
|
||||
faEnvelope,
|
||||
faEnvelopeOpen,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faEdit,
|
||||
faEye,
|
||||
faTrashAlt,
|
||||
faCheck,
|
||||
faCheckDouble,
|
||||
faTimes,
|
||||
faHome,
|
||||
faSave,
|
||||
faKeyboard,
|
||||
faMouse,
|
||||
faCog,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faGripHorizontal,
|
||||
faUserCog,
|
||||
faChevronDown,
|
||||
faExclamationTriangle,
|
||||
faQuestionCircle,
|
||||
faSearchPlus,
|
||||
faSearchMinus,
|
||||
faSearch,
|
||||
faPlus,
|
||||
faUser,
|
||||
faThumbtack,
|
||||
faPen,
|
||||
faCogs,
|
||||
faRemoveFormat,
|
||||
faBold,
|
||||
faItalic,
|
||||
faUnderline,
|
||||
faStrikethrough,
|
||||
faQuoteRight,
|
||||
faCode,
|
||||
faParagraph,
|
||||
faListOl,
|
||||
faListUl,
|
||||
faTasks,
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
faAlignCenter,
|
||||
faAlignJustify,
|
||||
faLink,
|
||||
faBars,
|
||||
faAngleUp,
|
||||
faAngleDown,
|
||||
faFilter,
|
||||
faAngleRight,
|
||||
faAngleLeft,
|
||||
faAngleDoubleLeft,
|
||||
faGripVertical,
|
||||
faEyeSlash,
|
||||
faQuestion,
|
||||
faEllipsisV,
|
||||
faTools,
|
||||
faClone,
|
||||
faDownload,
|
||||
faTable,
|
||||
faBell,
|
||||
faBellSolid,
|
||||
faBellSlash,
|
||||
faEnvelope,
|
||||
faEnvelopeOpen,
|
||||
)
|
||||
56
client/web/reporter/src/components/index.js
Normal file
56
client/web/reporter/src/components/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import Vue from 'vue'
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import PortalVue from 'portal-vue'
|
||||
import './faIcons'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
// import ECharts modules manually to reduce bundle size
|
||||
import ECharts from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import {
|
||||
SVGRenderer,
|
||||
} from 'echarts/renderers'
|
||||
import {
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
GaugeChart,
|
||||
RadarChart,
|
||||
FunnelChart,
|
||||
} from 'echarts/charts'
|
||||
|
||||
import {
|
||||
TitleComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent,
|
||||
VisualMapComponent,
|
||||
ToolboxComponent,
|
||||
} from 'echarts/components'
|
||||
|
||||
use([
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
GaugeChart,
|
||||
RadarChart,
|
||||
FunnelChart,
|
||||
SVGRenderer,
|
||||
TitleComponent,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
VisualMapComponent,
|
||||
ToolboxComponent,
|
||||
])
|
||||
|
||||
Vue.component('e-charts', ECharts)
|
||||
|
||||
Vue.use(PortalVue)
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
||||
Vue.component('font-awesome-layers', FontAwesomeLayers)
|
||||
Vue.component('c-permissions-button', components.CPermissionsButton)
|
||||
Vue.component('c-input-confirm', components.CInputConfirm)
|
||||
Vue.component('c-button-submit', components.CButtonSubmit)
|
||||
Vue.component('c-input-select', components.CInputSelect)
|
||||
Vue.component('c-form-table-wrapper', components.CFormTableWrapper)
|
||||
8
client/web/reporter/src/config-check.js
Normal file
8
client/web/reporter/src/config-check.js
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
'CortezaAPI',
|
||||
].forEach((cfg) => {
|
||||
if (window[cfg] === undefined) {
|
||||
throw new Error(`Missing or invalid configuration.
|
||||
Make sure there is a public/config.js configuration file with window.${cfg} entry.`)
|
||||
}
|
||||
})
|
||||
6
client/web/reporter/src/console-splash.js
Normal file
6
client/web/reporter/src/console-splash.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable no-undef */
|
||||
const text = `%c${WEBAPP || 'Corteza Webapp'}, version: ${VERSION}, build time: ${BUILD_TIME}`
|
||||
const style = 'background-color: #1397CB; color: white; padding: 3px 10px; border: 1px solid black; font: Courier'
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log(text, style)
|
||||
46
client/web/reporter/src/dev-env.js
Normal file
46
client/web/reporter/src/dev-env.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import c3catalogue from './components/C3'
|
||||
import { components, i18n } from '@cortezaproject/corteza-vue'
|
||||
import './components'
|
||||
import './themes'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/c3',
|
||||
name: 'c3',
|
||||
component: components.C3.View,
|
||||
props: { catalogue: c3catalogue },
|
||||
},
|
||||
{ path: '*', redirect: { name: 'c3' } },
|
||||
]
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default new Vue({
|
||||
el: '#app',
|
||||
name: 'DevEnv',
|
||||
template: '<router-view/>',
|
||||
router: new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
}),
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-reporter' },
|
||||
'builder',
|
||||
'create',
|
||||
'datasources',
|
||||
'display-element',
|
||||
'edit',
|
||||
'general',
|
||||
'list',
|
||||
'navigation',
|
||||
'notification',
|
||||
'permissions',
|
||||
'scenarios',
|
||||
'sidebar',
|
||||
'view',
|
||||
),
|
||||
})
|
||||
6
client/web/reporter/src/filters/index.js
Normal file
6
client/web/reporter/src/filters/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import { filters } from '@cortezaproject/corteza-vue'
|
||||
|
||||
for (const n in filters) {
|
||||
Vue.filter(n, filters[n])
|
||||
}
|
||||
4
client/web/reporter/src/main.js
Normal file
4
client/web/reporter/src/main.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import app from './app'
|
||||
import './themes'
|
||||
|
||||
app()
|
||||
5
client/web/reporter/src/mixins/index.js
Normal file
5
client/web/reporter/src/mixins/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import { mixins } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.mixin(mixins.toast)
|
||||
227
client/web/reporter/src/mixins/listHelpers.js
Normal file
227
client/web/reporter/src/mixins/listHelpers.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import axios from 'axios'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
/**
|
||||
* Placeholder, if component does not define own filter
|
||||
*/
|
||||
filter: {},
|
||||
|
||||
pagination: {
|
||||
limit: 100,
|
||||
pageCursor: undefined,
|
||||
prevPage: '',
|
||||
nextPage: '',
|
||||
total: 0,
|
||||
page: 1,
|
||||
},
|
||||
|
||||
// Used to save query when fetching for total if we came to the page with a pageCursor in URL
|
||||
tempQuery: undefined,
|
||||
|
||||
sorting: {},
|
||||
|
||||
abortableRequests: [],
|
||||
cancelled: false,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* When fullPath for this component changes, we most likely should update our
|
||||
* filters.
|
||||
* @todo make this.filter reactive
|
||||
*/
|
||||
'$route.fullPath': {
|
||||
handler () {
|
||||
this.handleQueryParams()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.handleQueryParams(true)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.abortRequests()
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
incLoader: 'ui/incLoader',
|
||||
decLoader: 'ui/decLoader',
|
||||
}),
|
||||
|
||||
/**
|
||||
* Parses query params into list filtering params.
|
||||
* @param initial {Boolean} - used to determine it this is the initial fetch
|
||||
*/
|
||||
handleQueryParams (initial = false) {
|
||||
// Pagination
|
||||
let {
|
||||
limit = this.pagination.limit,
|
||||
pageCursor = this.pagination.pageCursor,
|
||||
prevPage = this.pagination.prevCursor,
|
||||
nextPage = this.pagination.nextCursor,
|
||||
total = this.pagination.total,
|
||||
page = this.pagination.page,
|
||||
...r1
|
||||
} = this.$route.query
|
||||
|
||||
limit = parseInt(limit)
|
||||
total = parseInt(total)
|
||||
page = parseInt(page)
|
||||
|
||||
// If we came to the page with a pageCursor in the URL
|
||||
if (initial && pageCursor) {
|
||||
this.tempQuery = this.$route.query
|
||||
// Fetch replace query to trigger fetch of total number of items
|
||||
this.$router.replace({ query: { ...this.$route.query, limit: 1, pageCursor: undefined } })
|
||||
return
|
||||
}
|
||||
|
||||
/// To prevent extra list fetch, check if pageCursor is defined (not first page)
|
||||
const refresh = this.$route.query.pageCursor !== this.pagination.pageCursor
|
||||
this.pagination = { limit, pageCursor, prevPage, nextPage, total, page }
|
||||
|
||||
// Sorting
|
||||
let { sortBy = this.sorting.sortBy, sortDesc = this.sorting.sortDesc, ...r2 } = r1
|
||||
|
||||
sortDesc = sortDesc === true || sortDesc === 'true'
|
||||
|
||||
// Reset pageCursor when sort changes, except on first fetch (so we use the pageCursor from url)
|
||||
if (!initial && (sortBy !== this.sorting.sortBy || sortDesc !== this.sorting.sortDesc)) {
|
||||
this.pagination.pageCursor = ''
|
||||
this.pagination.page = 1
|
||||
}
|
||||
this.sorting = { sortBy, sortDesc }
|
||||
|
||||
// Filtering
|
||||
// make sure filter fields are of the right type
|
||||
for (const key in r2) {
|
||||
if (typeof this.filter[key] === 'boolean') {
|
||||
r2[key] = r2[key] === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
this.filter = { ...this.filter, ...r2 }
|
||||
|
||||
// Only refresh if pageCursor actually changed,
|
||||
if (refresh) {
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
}
|
||||
},
|
||||
|
||||
filterList () {
|
||||
// reset pagination when filtering changes
|
||||
//
|
||||
// we want to prevent situations with page is preset to a number that
|
||||
// exceeds the number of pages of a filtered results
|
||||
this.pagination.pageCursor = ''
|
||||
this.pagination.page = 1
|
||||
|
||||
// notify b-table about the change
|
||||
//
|
||||
// this effectively calls items()/procListResults()
|
||||
this.abortRequests()
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
},
|
||||
|
||||
encodeListParams () {
|
||||
let { sortBy, sortDesc } = this.sorting
|
||||
const { limit, pageCursor } = this.pagination
|
||||
|
||||
if (sortBy === 'changedAt') {
|
||||
sortBy = 'coalesce(deletedAt, updatedAt, createdAt)'
|
||||
}
|
||||
|
||||
const sort = sortBy ? `${sortBy} ${sortDesc ? 'DESC' : 'ASC'}` : undefined
|
||||
|
||||
return {
|
||||
limit,
|
||||
sort: pageCursor ? undefined : sort,
|
||||
...this.filter,
|
||||
pageCursor,
|
||||
incTotal: !pageCursor || this.tempQuery,
|
||||
}
|
||||
},
|
||||
|
||||
encodeRouteParams () {
|
||||
const { limit, pageCursor, page } = this.pagination
|
||||
|
||||
return {
|
||||
query: {
|
||||
limit,
|
||||
...this.sorting,
|
||||
...this.filter,
|
||||
page,
|
||||
pageCursor,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param p {Promise}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
procListResults (p, updateQuery = true) {
|
||||
this.abortRequests()
|
||||
|
||||
const { response, cancel } = p
|
||||
this.abortableRequests.push(cancel)
|
||||
|
||||
// Push new router/params to cause URL change
|
||||
//
|
||||
// We want this because in case when user refreshes or shares URL
|
||||
// he needs to land on the same page with the same parameters
|
||||
if (updateQuery && !this.tempQuery) {
|
||||
this.$router.replace(this.encodeRouteParams())
|
||||
}
|
||||
|
||||
return Promise.all([response(), new Promise(resolve => setTimeout(resolve, 300))])
|
||||
.then(async ([{ set, filter }]) => {
|
||||
if (filter.incTotal) {
|
||||
this.pagination.total = filter.total
|
||||
}
|
||||
|
||||
// This was a fetch of total number of items
|
||||
if (this.tempQuery) {
|
||||
const query = this.tempQuery
|
||||
this.tempQuery = undefined
|
||||
|
||||
this.$router.replace({ query })
|
||||
return []
|
||||
}
|
||||
|
||||
this.pagination.pageCursor = undefined
|
||||
this.pagination.nextPage = filter.nextPage
|
||||
this.pagination.prevPage = filter.prevPage
|
||||
|
||||
return set
|
||||
}).catch(error => {
|
||||
if (!axios.isCancel(error)) {
|
||||
this.toastErrorHandler(this.$t('notification:list.load.error'))(error)
|
||||
} else {
|
||||
this.cancelled = true
|
||||
}
|
||||
}).finally(() => {
|
||||
this.cancelled = false
|
||||
})
|
||||
},
|
||||
|
||||
genericRowClass (item) {
|
||||
return { 'text-secondary': item && !!item.deletedAt }
|
||||
},
|
||||
|
||||
abortRequests () {
|
||||
this.abortableRequests.forEach((cancel) => {
|
||||
cancel()
|
||||
})
|
||||
this.abortableRequests = []
|
||||
},
|
||||
},
|
||||
}
|
||||
121
client/web/reporter/src/mixins/report.js
Normal file
121
client/web/reporter/src/mixins/report.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { system } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
processingSave: false,
|
||||
processingDelete: false,
|
||||
processingClone: false,
|
||||
report: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchReport (reportID) {
|
||||
this.processing = true
|
||||
|
||||
return this.$SystemAPI.reportRead({ reportID })
|
||||
.then(report => {
|
||||
this.report = new system.Report(report)
|
||||
this.initialReportState = this.report.clone()
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:report.fetchFailed')))
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.processing = false
|
||||
}, 400)
|
||||
})
|
||||
},
|
||||
|
||||
async handleSave () {
|
||||
this.processing = true
|
||||
this.processingSave = true
|
||||
|
||||
const { blocks } = this.report
|
||||
|
||||
// Remove dataframes from elements before saving report
|
||||
const report = {
|
||||
...this.report,
|
||||
blocks: blocks.map(block => {
|
||||
block.elements = block.elements.map(element => {
|
||||
delete element.dataframes
|
||||
return element
|
||||
})
|
||||
return block
|
||||
}),
|
||||
}
|
||||
|
||||
// If new then create otherwise update
|
||||
if (this.isNew) {
|
||||
return this.$SystemAPI.reportCreate(report)
|
||||
.then(report => {
|
||||
this.report = new system.Report(report)
|
||||
this.initialReportState = this.report.clone()
|
||||
this.detectStateChange = false
|
||||
this.toastSuccess(this.$t('notification:report.created'))
|
||||
this.$router.push({ name: 'report.builder', params: { reportID: report.reportID } })
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:report.createFailed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
this.processingSave = false
|
||||
})
|
||||
} else {
|
||||
return this.$SystemAPI.reportUpdate(report)
|
||||
.then(report => {
|
||||
this.report = new system.Report(report)
|
||||
this.initialReportState = this.report.clone()
|
||||
this.detectStateChange = false
|
||||
this.toastSuccess(this.$t('notification:report.updated'))
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:report.updateFailed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
this.processingSave = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleDelete () {
|
||||
this.processing = true
|
||||
this.processingDelete = true
|
||||
|
||||
return this.$SystemAPI.reportDelete(this.report)
|
||||
.then(() => {
|
||||
this.report.deletedAt = new Date()
|
||||
this.toastSuccess(this.$t('notification:report.delete'))
|
||||
this.$router.push({ name: 'report.list' })
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:report.deleteFailed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
this.processingDelete = false
|
||||
})
|
||||
},
|
||||
|
||||
handleClone (report) {
|
||||
this.processing = true
|
||||
this.processingClone = true
|
||||
const { meta, sources, blocks, scenarios, labels } = report
|
||||
const cloneSuffix = `${meta.name} (${this.$t('general:cloneSuffix')})`
|
||||
let name = meta.name
|
||||
|
||||
if (!this.initialReportState || this.initialReportState.meta.name === name) {
|
||||
name = cloneSuffix
|
||||
}
|
||||
|
||||
return this.$SystemAPI.reportCreate({ handle: '', meta: { ...meta, name }, sources, blocks, scenarios, labels })
|
||||
.then(report => {
|
||||
report = new system.Report(report)
|
||||
this.toastSuccess(this.$t('notification:report.created'))
|
||||
return report
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:report.createFailed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
this.processingClone = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
26
client/web/reporter/src/plugins/index.js
Normal file
26
client/web/reporter/src/plugins/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
|
||||
import VueNativeSock from 'vue-native-websocket'
|
||||
import { plugins, websocket } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
Vue.use(BootstrapVue, {
|
||||
BToast: {
|
||||
// see https://bootstrap-vue.org/docs/components/toast#comp-ref-b-toast-props
|
||||
autoHideDelay: 7000,
|
||||
toaster: 'b-toaster-bottom-right',
|
||||
},
|
||||
BModal: {
|
||||
noEnforceFocus: true,
|
||||
},
|
||||
})
|
||||
Vue.use(BootstrapVueIcons)
|
||||
|
||||
Vue.use(plugins.Auth(), { app: 'reporter' })
|
||||
|
||||
Vue.use(plugins.CortezaAPI('system'))
|
||||
Vue.use(plugins.CortezaAPI('compose'))
|
||||
Vue.use(plugins.Settings, { api: Vue.prototype.$SystemAPI })
|
||||
Vue.use(VueNativeSock, websocket.endpoint(), websocket.config)
|
||||
7
client/web/reporter/src/router.js
Normal file
7
client/web/reporter/src/router.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Router from 'vue-router'
|
||||
import routes from './views/routes.js'
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
23
client/web/reporter/src/store/index.js
Normal file
23
client/web/reporter/src/store/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import { store as cvStore } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
rbac: {
|
||||
namespaced: true,
|
||||
...cvStore.RBAC(Vue.prototype.$SystemAPI),
|
||||
},
|
||||
notifications: {
|
||||
namespaced: true,
|
||||
...cvStore.notifications({
|
||||
api: Vue.prototype.$SystemAPI,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default store
|
||||
1
client/web/reporter/src/themes/corteza-base/index.js
Normal file
1
client/web/reporter/src/themes/corteza-base/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import './index.scss'
|
||||
2
client/web/reporter/src/themes/corteza-base/index.scss
Normal file
2
client/web/reporter/src/themes/corteza-base/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "./custom";
|
||||
@import "./poppins.scss";
|
||||
146
client/web/reporter/src/themes/corteza-base/poppins.scss
Normal file
146
client/web/reporter/src/themes/corteza-base/poppins.scss
Normal file
@@ -0,0 +1,146 @@
|
||||
// Fonts Path
|
||||
$fonts_dir : './fonts/' !default;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Bold';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Bold.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-BlackItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-BlackItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Italic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Italic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-BoldItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-BoldItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ExtraBoldItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ExtraBoldItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ExtraLightItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ExtraLightItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Black';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Black.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ExtraBold';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ExtraBold.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ExtraLight';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ExtraLight.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-MediumItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-MediumItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-SemiBoldItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-SemiBoldItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-SemiBold';
|
||||
src: url($fonts_dir + 'poppins/Poppins-SemiBold.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-LightItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-LightItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Regular';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Regular.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Medium';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Medium.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Light';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Light.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ThinItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ThinItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Thin';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Thin.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
2
client/web/reporter/src/themes/index.js
Normal file
2
client/web/reporter/src/themes/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
import(/* webpackChunkName: 'corteza-base' */ './corteza-base')
|
||||
198
client/web/reporter/src/views/Layout.vue
Normal file
198
client/web/reporter/src/views/Layout.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column w-100 h-viewport overflow-hidden">
|
||||
<header>
|
||||
<c-topbar
|
||||
:expanded="expanded"
|
||||
:settings="$Settings.get('ui.topbar', {})"
|
||||
:labels="{
|
||||
appMenu: $t('navigation:appMenu'),
|
||||
helpForum: $t('navigation:help.forum'),
|
||||
helpDocumentation: $t('navigation:help.documentation'),
|
||||
helpFeedback: $t('navigation:help.feedback'),
|
||||
helpVersion: $t('navigation:help.version'),
|
||||
userSettingsLoggedInAs: $t('navigation:userSettings.loggedInAs', { user }),
|
||||
userSettingsProfile: $t('navigation:userSettings.profile'),
|
||||
userSettingsChangePassword: $t('navigation:userSettings.changePassword'),
|
||||
userSettingsLogout: $t('navigation:userSettings.logout'),
|
||||
userSettingsTheme: $t('navigation:userSettings.theme'),
|
||||
lightTheme: $t('general:themes.labels.light'),
|
||||
darkTheme: $t('general:themes.labels.dark'),
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<portal-target
|
||||
name="topbar-title"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #tools>
|
||||
<portal-target
|
||||
name="topbar-tools"
|
||||
class="gap-1"
|
||||
/>
|
||||
</template>
|
||||
</c-topbar>
|
||||
</header>
|
||||
|
||||
<aside>
|
||||
<c-sidebar
|
||||
:expanded.sync="expanded"
|
||||
:icon="icon"
|
||||
:logo="logo"
|
||||
:disabled-routes="disabledRoutes"
|
||||
expand-on-click
|
||||
>
|
||||
<template #header-expanded>
|
||||
<portal-target name="sidebar-header-expanded" />
|
||||
</template>
|
||||
|
||||
<template #body-expanded>
|
||||
<portal-target name="sidebar-body-expanded" />
|
||||
</template>
|
||||
|
||||
<template #footer-expanded>
|
||||
<portal-target name="sidebar-footer-expanded" />
|
||||
</template>
|
||||
</c-sidebar>
|
||||
</aside>
|
||||
|
||||
<main class="d-inline-flex h-100 overflow-auto">
|
||||
<!--
|
||||
Content spacer
|
||||
Large and xl screens should push in content when the nav is expanded
|
||||
-->
|
||||
<template>
|
||||
<div
|
||||
class="sidebar-spacer d-print-none"
|
||||
:class="{
|
||||
'expanded': expanded,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="d-flex flex-column w-100"
|
||||
>
|
||||
<router-view
|
||||
class="flex-grow-1 overflow-auto"
|
||||
/>
|
||||
|
||||
<portal-target
|
||||
name="report-toolbar"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<c-permissions-modal
|
||||
:labels="{
|
||||
save: $t('permissions:ui.save'),
|
||||
cancel: $t('permissions:ui.cancel'),
|
||||
loading: $t('permissions:ui.loading'),
|
||||
edit: {
|
||||
label: $t('permissions:ui.edit.label'),
|
||||
description: $t('permissions:ui.edit.description'),
|
||||
},
|
||||
evaluate: {
|
||||
title: $t('permissions:ui.evaluate.title'),
|
||||
description: $t('permissions:ui.evaluate.description'),
|
||||
},
|
||||
add: {
|
||||
label: $t('permissions:ui.add.label'),
|
||||
title: $t('permissions:ui.add.title'),
|
||||
save: $t('permissions:ui.add.save'),
|
||||
role: {
|
||||
label: $t('permissions:ui.add.role.label'),
|
||||
placeholder: $t('permissions:ui.add.role.placeholder'),
|
||||
},
|
||||
user: {
|
||||
label: $t('permissions:ui.add.user.label'),
|
||||
placeholder: $t('permissions:ui.add.user.placeholder'),
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<report-sidebar />
|
||||
|
||||
<c-extend-session
|
||||
v-if="isAutoLogoutEnabled"
|
||||
:timeout="$Settings.get('auth.autoLogout.timeout')"
|
||||
:labels="{
|
||||
extend: $t('general:extendSession.labels.extend'),
|
||||
warning: (countdownTime) => $t('general:extendSession.labels.warning', { countdownTime }),
|
||||
}"
|
||||
/>
|
||||
|
||||
<c-notification-sidebar v-if="!$Settings.get('ui.topbar', {}).hideNotifications" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import ReportSidebar from 'corteza-webapp-reporter/src/components/ReportSidebar'
|
||||
const { CPermissionsModal, CTopbar, CSidebar, CExtendSession, CNotificationSidebar } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CPermissionsModal,
|
||||
CTopbar,
|
||||
CSidebar,
|
||||
ReportSidebar,
|
||||
CExtendSession,
|
||||
CNotificationSidebar,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
expanded: false,
|
||||
|
||||
disabledRoutes: [
|
||||
'report.list',
|
||||
'report.create',
|
||||
'report.edit',
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
user () {
|
||||
const { user } = this.$auth
|
||||
return user.name || user.handle || user.email || ''
|
||||
},
|
||||
|
||||
icon () {
|
||||
return this.$Settings.attachment('ui.iconLogo')
|
||||
},
|
||||
|
||||
logo () {
|
||||
return this.$Settings.attachment('ui.mainLogo')
|
||||
},
|
||||
|
||||
isAutoLogoutEnabled () {
|
||||
return this.$Settings.get('auth.autoLogout.enabled')
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$root.$on('alert', this.displayToast)
|
||||
},
|
||||
|
||||
methods: {
|
||||
displayToast ({ title, message, variant, countdown }) {
|
||||
this.$bvToast.toast(message, {
|
||||
title,
|
||||
variant,
|
||||
solid: true,
|
||||
autoHideDelay: countdown,
|
||||
toaster: 'b-toaster-bottom-right',
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.h-viewport {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
}
|
||||
</style>
|
||||
1052
client/web/reporter/src/views/Report/Builder.vue
Normal file
1052
client/web/reporter/src/views/Report/Builder.vue
Normal file
File diff suppressed because it is too large
Load Diff
341
client/web/reporter/src/views/Report/Edit.vue
Normal file
341
client/web/reporter/src/views/Report/Edit.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<div class="py-3">
|
||||
<portal to="topbar-title">
|
||||
<div
|
||||
class="d-flex w-100"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<portal to="topbar-tools">
|
||||
<b-button-group
|
||||
v-if="report && !isNew"
|
||||
size="sm"
|
||||
>
|
||||
<b-button
|
||||
variant="primary"
|
||||
data-test-id="button-report-builder"
|
||||
class="d-flex align-items-center justify-content-center"
|
||||
:disabled="!report.canUpdateReport"
|
||||
:to="reportBuilder"
|
||||
>
|
||||
{{ $t('report.builder') }}
|
||||
<font-awesome-icon
|
||||
class="ml-2"
|
||||
:icon="['fas', 'tools']"
|
||||
/>
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('tooltip.view-report'), boundary: 'body' }"
|
||||
variant="primary"
|
||||
style="margin-left:2px;"
|
||||
:disabled="!canRead"
|
||||
:to="reportViewer"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'eye']"
|
||||
/>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</portal>
|
||||
|
||||
<b-container v-if="report">
|
||||
<b-row no-gutters>
|
||||
<b-col>
|
||||
<b-card
|
||||
no-body
|
||||
show
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-card-header
|
||||
v-if="!isNew"
|
||||
header-bg-variant="white border-bottom"
|
||||
>
|
||||
<b-row
|
||||
no-gutters
|
||||
class="align-items-center"
|
||||
>
|
||||
<div>
|
||||
<b-button
|
||||
v-if="canCreate"
|
||||
data-test-id="button-create-report"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
class="mr-1"
|
||||
:to="{ name: 'report.create' }"
|
||||
>
|
||||
{{ $t('new-report') }}
|
||||
</b-button>
|
||||
|
||||
<c-permissions-button
|
||||
v-if="canGrant"
|
||||
:title="report.meta.name || report.handle || report.reportID"
|
||||
:target="report.meta.name || report.handle || report.reportID"
|
||||
:resource="`corteza::system:report/${report.reportID}`"
|
||||
:button-label="$t('permissions')"
|
||||
class="btn-lg ml-1"
|
||||
/>
|
||||
</div>
|
||||
</b-row>
|
||||
</b-card-header>
|
||||
|
||||
<b-container
|
||||
fluid
|
||||
class="py-3"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="4"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name-with-star')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="report.meta.name"
|
||||
data-test-id="input-name"
|
||||
:placeholder="$t('name')"
|
||||
required
|
||||
:state="nameState"
|
||||
@input="handleDetectStateChange"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="4"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('handle-with-star')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="report.handle"
|
||||
data-test-id="input-handle"
|
||||
:placeholder="$t('placeholder-handle')"
|
||||
required
|
||||
:state="handleState"
|
||||
@input="handleDetectStateChange"
|
||||
/>
|
||||
<b-form-invalid-feedback
|
||||
data-test-id="input-handle-invalid-state"
|
||||
:state="handleState"
|
||||
>
|
||||
{{ $t('invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('description')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="report.meta.description"
|
||||
data-test-id="input-description"
|
||||
:placeholder="$t('report.description')"
|
||||
rows="5"
|
||||
@input="handleDetectStateChange"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<!-- <b-form-group
|
||||
label="Tags"
|
||||
class="text-primary"
|
||||
>
|
||||
<b-form-tags
|
||||
v-model="report.meta.tags"
|
||||
/>
|
||||
</b-form-group> -->
|
||||
</b-container>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="d-flex align-items-center justify-content-center w-100 h-100"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<portal to="report-toolbar">
|
||||
<editor-toolbar
|
||||
:back-link="{ name: 'report.list' }"
|
||||
:hide-delete="isNew"
|
||||
:delete-disabled="!canDelete"
|
||||
:save-disabled="!canSave"
|
||||
:clone-disabled="!canSave"
|
||||
:processing="processing"
|
||||
:processing-save="processingSave"
|
||||
:processing-delete="processingDelete"
|
||||
:processing-clone="processingClone"
|
||||
@clone="handleReportCloning"
|
||||
@delete="handleDelete"
|
||||
@save="handleSave"
|
||||
/>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { system } from '@cortezaproject/corteza-js'
|
||||
import { handle } from '@cortezaproject/corteza-vue'
|
||||
import report from 'corteza-webapp-reporter/src/mixins/report'
|
||||
import EditorToolbar from 'corteza-webapp-reporter/src/components/EditorToolbar'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'EditReport',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'edit',
|
||||
},
|
||||
|
||||
components: {
|
||||
EditorToolbar,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
report,
|
||||
],
|
||||
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.checkUnsavedChart(next)
|
||||
},
|
||||
|
||||
beforeRouteLeave (to, from, next) {
|
||||
this.checkUnsavedChart(next)
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
|
||||
report: undefined,
|
||||
initialReportState: undefined,
|
||||
|
||||
detectStateChange: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
can: 'rbac/can',
|
||||
}),
|
||||
|
||||
canGrant () {
|
||||
return this.can('system/', 'grant')
|
||||
},
|
||||
|
||||
canCreate () {
|
||||
return this.can('system/', 'report.create')
|
||||
},
|
||||
|
||||
canRead () {
|
||||
return this.report ? this.report.canReadReport : false
|
||||
},
|
||||
|
||||
canDelete () {
|
||||
return this.report ? this.report.canDeleteReport : false
|
||||
},
|
||||
|
||||
canUpdate () {
|
||||
return this.isNew ? this.canCreate : (this.report && this.report.canUpdateReport) || false
|
||||
},
|
||||
|
||||
reportID () {
|
||||
return this.$route.params.reportID
|
||||
},
|
||||
|
||||
isNew () {
|
||||
return !this.reportID
|
||||
},
|
||||
|
||||
pageTitle () {
|
||||
return this.isNew ? this.$t('report.create') : this.$t('report.edit')
|
||||
},
|
||||
|
||||
reportBuilder () {
|
||||
return this.report ? { name: 'report.builder', params: { reportID: this.report.reportID } } : undefined
|
||||
},
|
||||
|
||||
reportViewer () {
|
||||
return this.report ? { name: 'report.view', params: { reportID: this.report.reportID } } : undefined
|
||||
},
|
||||
|
||||
nameState () {
|
||||
const { name = '' } = this.report.meta
|
||||
return name.length ? null : false
|
||||
},
|
||||
|
||||
handleState () {
|
||||
return handle.handleState(this.report.handle)
|
||||
},
|
||||
|
||||
canSave () {
|
||||
return this.canUpdate && ![this.nameState, this.handleState].includes(false)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
reportID: {
|
||||
immediate: true,
|
||||
handler (reportID) {
|
||||
// Fetch report or make a new one
|
||||
if (reportID) {
|
||||
this.fetchReport(reportID)
|
||||
} else {
|
||||
this.report = new system.Report()
|
||||
this.initialReportState = new system.Report()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleDetectStateChange () {
|
||||
this.detectStateChange = true
|
||||
},
|
||||
|
||||
checkUnsavedChart (next) {
|
||||
if (this.report.deletedAt) {
|
||||
return next(true)
|
||||
}
|
||||
|
||||
const reportState = {
|
||||
handle: this.report.handle,
|
||||
meta: {
|
||||
name: this.report.meta.name,
|
||||
description: this.report.meta.description,
|
||||
},
|
||||
}
|
||||
|
||||
const initialReportState = {
|
||||
handle: this.initialReportState.handle,
|
||||
meta: {
|
||||
name: this.initialReportState.meta.name,
|
||||
description: this.initialReportState.meta.description,
|
||||
},
|
||||
}
|
||||
|
||||
next(!isEqual(reportState, initialReportState) ? window.confirm(this.$t('unsavedChanges')) : true)
|
||||
},
|
||||
|
||||
handleReportCloning () {
|
||||
this.handleClone(this.report).then(({ reportID }) => {
|
||||
this.$router.push({ name: 'report.builder', params: { reportID } })
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
262
client/web/reporter/src/views/Report/List.vue
Normal file
262
client/web/reporter/src/views/Report/List.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<b-container
|
||||
fluid="xl"
|
||||
class="d-flex flex-column py-3"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t('report.list') }}
|
||||
</portal>
|
||||
|
||||
<c-resource-list
|
||||
:primary-key="primaryKey"
|
||||
:filter="filter"
|
||||
:sorting="sorting"
|
||||
:pagination="pagination"
|
||||
:fields="tableFields"
|
||||
:items="reportList"
|
||||
:translations="{
|
||||
searchPlaceholder: $t('searchPlaceholder'),
|
||||
notFound: $t('general:resourceList.notFound'),
|
||||
noItems: $t('general:resourceList.noItems'),
|
||||
loading: $t('general:label.loading'),
|
||||
showingPagination: 'general:resourceList.pagination.showing',
|
||||
singlePluralPagination: 'general:resourceList.pagination.single',
|
||||
prevPagination: $t('general:resourceList.pagination.prev'),
|
||||
nextPagination: $t('general:resourceList.pagination.next'),
|
||||
resourceSingle: $t('general:label.report.single'),
|
||||
resourcePlural: $t('general:label.report.plural')
|
||||
}"
|
||||
sticky-header
|
||||
clickable
|
||||
class="h-100 flex-fill"
|
||||
@search="filterList"
|
||||
@row-clicked="viewReport"
|
||||
>
|
||||
<template #header>
|
||||
<b-button
|
||||
v-if="canCreate"
|
||||
data-test-id="button-create-report"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
:to="{ name: 'report.create' }"
|
||||
>
|
||||
{{ $t('report.new') }}
|
||||
</b-button>
|
||||
|
||||
<c-permissions-button
|
||||
v-if="canGrant"
|
||||
resource="corteza::system:report/*"
|
||||
:button-label="$t('permissions')"
|
||||
size="lg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #name="{ item: r }">
|
||||
{{ r.meta.name }}
|
||||
</template>
|
||||
|
||||
<template #changedAt="{ item }">
|
||||
{{ (item.deletedAt || item.updatedAt || item.createdAt) | locFullDateTime }}
|
||||
</template>
|
||||
|
||||
<template #actions="{ item: r }">
|
||||
<b-button-group
|
||||
v-if="r.canUpdateReport"
|
||||
size="sm"
|
||||
>
|
||||
<b-button
|
||||
data-test-id="button-report-builder"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:to="{ name: 'report.builder', params: { reportID: r.reportID } }"
|
||||
>
|
||||
{{ $t('report.builder') }}
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'tools']"
|
||||
class="ml-2"
|
||||
/>
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('report.edit'), boundary: 'body' }"
|
||||
data-test-id="button-report-edit"
|
||||
variant="primary"
|
||||
:to="{ name: 'report.edit', params: { reportID: r.reportID } }"
|
||||
class="d-flex align-items-center"
|
||||
style="margin-left:2px;"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'edit']"
|
||||
/>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
|
||||
<b-dropdown
|
||||
v-if="r.canUpdateReport || r.canGrant || r.canDeleteReport"
|
||||
variant="outline-extra-light"
|
||||
toggle-class="d-flex align-items-center justify-content-center text-primary border-0 py-2"
|
||||
no-caret
|
||||
lazy
|
||||
menu-class="m-0"
|
||||
class="ml-2"
|
||||
>
|
||||
<template #button-content>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'ellipsis-v']"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item-button @click="handleReportCloning(r)">
|
||||
<font-awesome-icon
|
||||
:icon="['fa','clone']"
|
||||
/>
|
||||
{{ $t('general:resourceList.clone') }}
|
||||
</b-dropdown-item-button>
|
||||
|
||||
<c-permissions-button
|
||||
v-if="r.canGrant"
|
||||
:tooltip="$t('permissions:resources.system.report.tooltip')"
|
||||
:title="r.meta.name || r.handle || r.reportID"
|
||||
:target="r.meta.name || r.handle || r.reportID"
|
||||
:resource="`corteza::system:report/${r.reportID}`"
|
||||
:button-label="$t('permissions:ui.label')"
|
||||
class="dropdown-item"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
v-if="r.canDeleteReport"
|
||||
:processing="processingDelete"
|
||||
:text="$t('report.delete')"
|
||||
borderless
|
||||
variant="link"
|
||||
size="md"
|
||||
show-icon
|
||||
text-class="p-1"
|
||||
button-class="dropdown-item"
|
||||
icon-class="text-danger"
|
||||
class="w-100"
|
||||
@confirmed="handleDelete(r)"
|
||||
/>
|
||||
</b-dropdown>
|
||||
</template>
|
||||
</c-resource-list>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import listHelpers from 'corteza-webapp-reporter/src/mixins/listHelpers'
|
||||
import report from 'corteza-webapp-reporter/src/mixins/report'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CResourceList } = components
|
||||
|
||||
export default {
|
||||
name: 'ReportList',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'list',
|
||||
},
|
||||
|
||||
components: {
|
||||
CResourceList,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
listHelpers,
|
||||
report,
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
primaryKey: 'reportID',
|
||||
|
||||
processingDelete: false,
|
||||
|
||||
filter: {
|
||||
query: '',
|
||||
},
|
||||
|
||||
sorting: {
|
||||
sortBy: 'handle',
|
||||
sortDesc: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
can: 'rbac/can',
|
||||
}),
|
||||
|
||||
canGrant () {
|
||||
return this.can('system/', 'grant')
|
||||
},
|
||||
|
||||
canCreate () {
|
||||
return this.can('system/', 'report.create')
|
||||
},
|
||||
|
||||
tableFields () {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: this.$t('columns.name'),
|
||||
sortable: false,
|
||||
tdClass: 'text-nowrap',
|
||||
},
|
||||
{
|
||||
key: 'handle',
|
||||
label: this.$t('columns.handle'),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'changedAt',
|
||||
label: this.$t('columns.changedAt'),
|
||||
sortable: true,
|
||||
class: 'text-right text-nowrap',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-nowrap actions gap-1',
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
viewReport ({ reportID, canReadReport = false }) {
|
||||
if (reportID) {
|
||||
this.$router.push({
|
||||
name: 'report.view',
|
||||
params: { reportID },
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
reportList () {
|
||||
return this.procListResults(this.$SystemAPI.reportListCancellable(this.encodeListParams()))
|
||||
},
|
||||
|
||||
handleDelete (report) {
|
||||
this.processingDelete = true
|
||||
|
||||
return this.$SystemAPI.reportDelete(report)
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('notification:report.delete'))
|
||||
this.filterList()
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:report.deleteFailed')))
|
||||
.finally(() => {
|
||||
this.processingDelete = false
|
||||
})
|
||||
},
|
||||
|
||||
handleReportCloning (report) {
|
||||
this.handleClone(report).then(({ reportID }) => {
|
||||
this.viewReport({ reportID })
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
208
client/web/reporter/src/views/Report/View.vue
Normal file
208
client/web/reporter/src/views/Report/View.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex overflow-auto p-2 w-100"
|
||||
>
|
||||
<portal
|
||||
v-if="!fetchingReport"
|
||||
to="topbar-title"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
</portal>
|
||||
|
||||
<portal to="topbar-tools">
|
||||
<c-input-select
|
||||
v-if="scenarioOptions.length"
|
||||
v-model="scenarios.selected"
|
||||
:options="scenarioOptions"
|
||||
:get-option-key="getOptionKey"
|
||||
:placeholder="$t('pick-scenario')"
|
||||
:disabled="processing || fetchingReport"
|
||||
size="sm"
|
||||
style="max-width: 300px; min-width: 150px;"
|
||||
@input="refreshReport()"
|
||||
/>
|
||||
|
||||
<b-button-group
|
||||
v-if="canUpdate"
|
||||
size="sm"
|
||||
>
|
||||
<b-button
|
||||
variant="primary"
|
||||
class="d-flex align-items-center justify-content-center"
|
||||
:to="reportBuilder"
|
||||
>
|
||||
{{ $t('report.builder') }}
|
||||
<font-awesome-icon
|
||||
class="ml-2"
|
||||
:icon="['fas', 'tools']"
|
||||
/>
|
||||
</b-button>
|
||||
<b-button
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('tooltip.edit.report'), boundary: 'body' }"
|
||||
variant="primary"
|
||||
class="d-flex align-items-center justify-content-center"
|
||||
style="margin-left:2px;"
|
||||
:to="reportEditor"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'edit']"
|
||||
/>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</portal>
|
||||
|
||||
<div
|
||||
v-if="fetchingReport"
|
||||
class="d-flex align-items-center justify-content-center w-100 h-100"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<grid
|
||||
v-if="report && canRead && showReport && !fetchingReport"
|
||||
:blocks="report.blocks"
|
||||
>
|
||||
<template
|
||||
slot-scope="{ block, index }"
|
||||
>
|
||||
<block
|
||||
:index="index"
|
||||
:block="block"
|
||||
:scenario="currentSelectedScenario"
|
||||
:report-i-d="reportID"
|
||||
/>
|
||||
</template>
|
||||
</grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { system } from '@cortezaproject/corteza-js'
|
||||
import Grid from 'corteza-webapp-reporter/src/components/Report/Grid'
|
||||
import Block from 'corteza-webapp-reporter/src/components/Report/Blocks'
|
||||
|
||||
export default {
|
||||
name: 'ReportView',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'view',
|
||||
},
|
||||
|
||||
components: {
|
||||
Grid,
|
||||
Block,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
showReport: true,
|
||||
fetchingReport: false,
|
||||
|
||||
report: undefined,
|
||||
dataframes: [],
|
||||
|
||||
scenarios: {
|
||||
selected: undefined,
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
reportID () {
|
||||
return this.$route.params.reportID
|
||||
},
|
||||
|
||||
pageTitle () {
|
||||
const title = this.report ? (this.report.meta.name || this.report.handle) : ''
|
||||
return title || this.$t('report.view')
|
||||
},
|
||||
|
||||
canRead () {
|
||||
return this.report ? this.report.canReadReport : false
|
||||
},
|
||||
|
||||
canUpdate () {
|
||||
return this.isNew ? this.canCreate : (this.report && this.report.canUpdateReport) || false
|
||||
},
|
||||
|
||||
reportBuilder () {
|
||||
return this.report ? { name: 'report.builder', params: { reportID: this.report.reportID } } : undefined
|
||||
},
|
||||
|
||||
reportEditor () {
|
||||
return this.report ? { name: 'report.edit', params: { reportID: this.report.reportID } } : undefined
|
||||
},
|
||||
|
||||
reportDatasources () {
|
||||
return this.report ? this.report.sources : []
|
||||
},
|
||||
|
||||
reportScenarios () {
|
||||
return this.report ? this.report.scenarios : []
|
||||
},
|
||||
|
||||
scenarioOptions () {
|
||||
return this.report ? this.reportScenarios.map(({ label }) => label) : []
|
||||
},
|
||||
|
||||
currentSelectedScenario () {
|
||||
return this.scenarios.selected ? this.reportScenarios.find(({ label }) => label === this.scenarios.selected) : undefined
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
reportID: {
|
||||
immediate: true,
|
||||
handler (reportID) {
|
||||
this.scenarios.selected = undefined
|
||||
this.report = undefined
|
||||
|
||||
if (reportID) {
|
||||
this.fetchingReport = true
|
||||
this.processing = true
|
||||
this.fetchReport(reportID)
|
||||
.catch(() => {
|
||||
this.toastErrorHandler(this.$t('notification:report.loadFailed'))
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.fetchingReport = false
|
||||
this.processing = false
|
||||
}, 400)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshReport () {
|
||||
this.showReport = false
|
||||
return setTimeout(() => {
|
||||
this.showReport = true
|
||||
}, 50)
|
||||
},
|
||||
|
||||
async fetchReport (reportID) {
|
||||
this.report = undefined
|
||||
|
||||
return this.$SystemAPI.reportRead({ reportID })
|
||||
.then(report => {
|
||||
this.report = new system.Report(report)
|
||||
|
||||
this.report.blocks = this.report.blocks.map(({ xywh, ...p }, i) => {
|
||||
const [x, y, w, h] = xywh
|
||||
return { ...p, x, y, w, h, i }
|
||||
})
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:report.fetchFailed')))
|
||||
},
|
||||
|
||||
getOptionKey (scenario) {
|
||||
return scenario
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
17
client/web/reporter/src/views/routes.js
Normal file
17
client/web/reporter/src/views/routes.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
name: 'root',
|
||||
redirect: { name: 'report.list' },
|
||||
component: () => import('./Layout'),
|
||||
children: [
|
||||
{ name: 'report.list', path: 'list', component: () => import('./Report/List') },
|
||||
{ name: 'report.create', path: 'new', component: () => import('./Report/Edit') },
|
||||
{ name: 'report.view', path: '/:reportID', component: () => import('./Report/View') },
|
||||
{ name: 'report.edit', path: '/:reportID/edit', component: () => import('./Report/Edit') },
|
||||
{ name: 'report.builder', path: '/:reportID/builder', component: () => import('./Report/Builder') },
|
||||
],
|
||||
},
|
||||
|
||||
{ path: '*', redirect: { name: 'root' } },
|
||||
]
|
||||
31
client/web/reporter/tests/setup.js
Normal file
31
client/web/reporter/tests/setup.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/* global jest */
|
||||
|
||||
jest.mock('@cortezaproject/corteza-js', () => ({}), { virtual: true })
|
||||
jest.mock('@cortezaproject/corteza-vue', () => ({
|
||||
components: {
|
||||
CToaster: jest.fn(),
|
||||
CPrompts: {
|
||||
name: 'c-prompts',
|
||||
render: () => {},
|
||||
},
|
||||
CPermissionsModal: {
|
||||
name: 'c-permissions-modal',
|
||||
render: () => {},
|
||||
},
|
||||
CTopbar: {
|
||||
name: 'c-topbar',
|
||||
render: () => {},
|
||||
},
|
||||
CSidebar: {
|
||||
name: 'c-sidebar',
|
||||
render: () => {},
|
||||
},
|
||||
},
|
||||
mixins: {
|
||||
corredor: {
|
||||
methods: {
|
||||
triggerScript: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}), { virtual: true })
|
||||
7
client/web/reporter/tests/unit/.eslintrc.js
Normal file
7
client/web/reporter/tests/unit/.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true,
|
||||
},
|
||||
}
|
||||
78
client/web/reporter/tests/unit/views/layout.spec.js
Normal file
78
client/web/reporter/tests/unit/views/layout.spec.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* global jest */
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils'
|
||||
import Layout from '../../../src/views/Layout'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import PortalVue from 'portal-vue'
|
||||
import Vuex from 'vuex'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('Layout.vue', () => {
|
||||
let localVue
|
||||
let store
|
||||
let rbacModule
|
||||
|
||||
beforeEach(() => {
|
||||
localVue = createLocalVue()
|
||||
localVue.use(BootstrapVue)
|
||||
localVue.use(PortalVue)
|
||||
localVue.use(Vuex)
|
||||
|
||||
// Mock the Vuex store
|
||||
rbacModule = {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
can: () => () => true,
|
||||
},
|
||||
}
|
||||
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
rbac: rbacModule,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallowMount(Layout, {
|
||||
localVue,
|
||||
store,
|
||||
mocks: {
|
||||
$auth: {
|
||||
user: {},
|
||||
},
|
||||
$Settings: {
|
||||
get: () => ({}),
|
||||
attachment: () => '',
|
||||
},
|
||||
$t: (key) => key,
|
||||
textDirectionality: () => 'ltr',
|
||||
$root: {
|
||||
$on: jest.fn(),
|
||||
},
|
||||
},
|
||||
stubs: [
|
||||
'router-view',
|
||||
'portal-target',
|
||||
'c-topbar',
|
||||
'c-sidebar',
|
||||
'c-prompts',
|
||||
'c-permissions-modal',
|
||||
],
|
||||
})
|
||||
|
||||
expect(wrapper.find('div').exists()).to.be.true
|
||||
expect(wrapper.find('div').classes()).to.include.members([
|
||||
'd-flex',
|
||||
'flex-column',
|
||||
'w-100',
|
||||
'h-viewport',
|
||||
'overflow-hidden',
|
||||
])
|
||||
})
|
||||
})
|
||||
239
client/web/reporter/vue.config-builder.js
Normal file
239
client/web/reporter/vue.config-builder.js
Normal file
@@ -0,0 +1,239 @@
|
||||
const webpack = require('webpack')
|
||||
const exec = require('child_process').execSync
|
||||
const path = require('path')
|
||||
const Vue = require('vue')
|
||||
|
||||
module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, theme, packageAlias, root = path.resolve('.'), env = process.env.NODE_ENV }) => {
|
||||
const isDevelopment = (env === 'development')
|
||||
const isTest = (env === 'test')
|
||||
|
||||
if (isTest) {
|
||||
Vue.config.devtools = false
|
||||
Vue.config.productionTip = false
|
||||
}
|
||||
|
||||
if (isDevelopment) {
|
||||
Vue.config.devtools = true
|
||||
Vue.config.performance = true
|
||||
}
|
||||
|
||||
const optimization = isTest
|
||||
? {}
|
||||
: {
|
||||
usedExports: true,
|
||||
runtimeChunk: 'single',
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 20000,
|
||||
maxSize: 244000,
|
||||
cacheGroups: {
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
chunks: 'all',
|
||||
priority: -10,
|
||||
},
|
||||
common: {
|
||||
minChunks: 2,
|
||||
priority: -20,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
publicPath: isDevelopment ? '/' : './',
|
||||
lintOnSave: true,
|
||||
runtimeCompiler: true,
|
||||
|
||||
configureWebpack: {
|
||||
// other webpack options to merge in ...
|
||||
|
||||
// Webpack 5 specific configuration
|
||||
resolve: {
|
||||
symlinks: true,
|
||||
fallback: {
|
||||
path: false,
|
||||
fs: false,
|
||||
crypto: false,
|
||||
stream: false,
|
||||
util: false,
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
FLAVOUR: JSON.stringify(appFlavour),
|
||||
WEBAPP: JSON.stringify(appLabel),
|
||||
VERSION: JSON.stringify(version || ('' + exec('git describe --always --tags')).trim()),
|
||||
BUILD_TIME: JSON.stringify((new Date()).toISOString()),
|
||||
}),
|
||||
],
|
||||
|
||||
optimization: {
|
||||
...optimization,
|
||||
moduleIds: 'deterministic',
|
||||
chunkIds: 'named',
|
||||
emitOnErrors: false,
|
||||
},
|
||||
|
||||
stats: {
|
||||
warnings: false,
|
||||
errors: false,
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: isDevelopment ? false : 'warning',
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000,
|
||||
},
|
||||
},
|
||||
|
||||
chainWebpack: config => {
|
||||
// Remove css extraction issues
|
||||
// https://github.com/vuejs/vue-cli/issues/3771#issuecomment-526228100
|
||||
config.plugin('friendly-errors').tap(args => {
|
||||
const vueCli3Transformer = args[0].additionalTransformers[0]
|
||||
args[0].additionalTransformers = [
|
||||
vueCli3Transformer,
|
||||
error => {
|
||||
return /\[mini-css-extract-plugin\]/.test(error.message) ? {} : error
|
||||
},
|
||||
]
|
||||
return args
|
||||
})
|
||||
|
||||
// Update copy-webpack-plugin configuration for v8+
|
||||
config.plugins.has('copy') && config.plugin('copy').tap(options => {
|
||||
options[0].patterns = [{
|
||||
from: 'public',
|
||||
globOptions: {
|
||||
ignore: ['**/config*js', '**/*gitignore', '**/index.html'],
|
||||
},
|
||||
noErrorOnMissing: true,
|
||||
}]
|
||||
return options
|
||||
})
|
||||
|
||||
// Aliasing full package name instead of '@' so we do
|
||||
// not break imports on apps that import this code
|
||||
config.resolve.alias.delete('@')
|
||||
if (packageAlias) {
|
||||
config.resolve.alias.set(packageAlias, root)
|
||||
}
|
||||
|
||||
if (isTest) {
|
||||
const scssRule = config.module.rule('scss')
|
||||
scssRule.uses.clear()
|
||||
scssRule
|
||||
.use('null-loader')
|
||||
.loader('null-loader')
|
||||
}
|
||||
|
||||
const scssNormal = config.module.rule('scss').oneOf('normal')
|
||||
|
||||
scssNormal.use('sass-loader')
|
||||
.loader('sass-loader')
|
||||
.tap(options => ({
|
||||
...options,
|
||||
sourceMap: true,
|
||||
sassOptions: {
|
||||
outputStyle: isDevelopment ? 'expanded' : 'compressed',
|
||||
},
|
||||
}))
|
||||
|
||||
// Load CSS assets according to their location
|
||||
scssNormal.use('resolve-url-loader')
|
||||
.loader('resolve-url-loader').options({
|
||||
keepQuery: true,
|
||||
removeCR: true,
|
||||
root: path.join(root, 'src/themes', theme),
|
||||
})
|
||||
.before('sass-loader')
|
||||
|
||||
// Keep this to ensure we don't have multiple HTML plugins
|
||||
},
|
||||
|
||||
devServer: {
|
||||
host: '127.0.0.1',
|
||||
port: 8080,
|
||||
hot: true,
|
||||
allowedHosts: 'all',
|
||||
webSocketServer: 'ws',
|
||||
liveReload: false,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
|
||||
proxy: {
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
watchFiles: {
|
||||
paths: [
|
||||
'**/*',
|
||||
'!**/node_modules/!(@cortezaproject)/**',
|
||||
],
|
||||
options: {
|
||||
usePolling: true,
|
||||
aggregateTimeout: 200,
|
||||
poll: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
client: {
|
||||
overlay: false,
|
||||
progress: true,
|
||||
},
|
||||
},
|
||||
|
||||
css: {
|
||||
sourceMap: isDevelopment,
|
||||
extract: !isTest,
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
sassOptions: {
|
||||
outputStyle: isDevelopment ? 'expanded' : 'compressed',
|
||||
},
|
||||
},
|
||||
postcss: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
require('autoprefixer'),
|
||||
require('rtlcss'),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function fetchBaseUrl () {
|
||||
const fs = require('fs')
|
||||
const window = {}
|
||||
|
||||
const fileContents =
|
||||
fs.existsSync('public/config.js')
|
||||
? fs.readFileSync('public/config.js', 'utf-8')
|
||||
: ''
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(fileContents)
|
||||
|
||||
const u = window.CortezaAPI || ''
|
||||
const ur = new URL(u.startsWith('//') ? `http:${u}` : u)
|
||||
|
||||
return `${ur.protocol}//${ur.host}/`
|
||||
} catch (e) {
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
9
client/web/reporter/vue.config.js
Normal file
9
client/web/reporter/vue.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const buildVueConfig = require('./vue.config-builder')
|
||||
|
||||
module.exports = buildVueConfig({
|
||||
appFlavour: 'Reporter',
|
||||
appName: 'reporter',
|
||||
appLabel: 'Corteza Reporter Editor',
|
||||
theme: 'corteza-base',
|
||||
packageAlias: 'corteza-webapp-reporter',
|
||||
})
|
||||
Reference in New Issue
Block a user