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/discovery/.browserslistrc
Normal file
3
client/web/discovery/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
16
client/web/discovery/.editorconfig
Normal file
16
client/web/discovery/.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/discovery/.eslintrc.js
Normal file
10
client/web/discovery/.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/discovery/.gitignore
vendored
Normal file
20
client/web/discovery/.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/discovery/Makefile
Normal file
39
client/web/discovery/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-discovery
|
||||
|
||||
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/discovery/babel.config.js
Normal file
5
client/web/discovery/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@vue/app', { useBuiltIns: 'entry' }],
|
||||
],
|
||||
}
|
||||
36
client/web/discovery/jest.config.js
Normal file
36
client/web/discovery/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-discovery/(.*)$': '<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/discovery/package.json
Normal file
87
client/web/discovery/package.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "corteza-discovery",
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"axios": "^1.8.3",
|
||||
"bootstrap-vue": "2.23.1",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"portal-vue": "^2.1.7",
|
||||
"vue": "2.7.16",
|
||||
"vue-native-websocket": "^2.0.15",
|
||||
"vue-router": "^3.4.9",
|
||||
"vue-simple-markdown": "^1.1.5",
|
||||
"vue-split-panel": "^1.0.4",
|
||||
"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",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"jest-transform-stub": "^2.0.0",
|
||||
"jest-watch-typeahead": "^1.0.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"
|
||||
},
|
||||
"stylelint": {
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"color-hex-length": null,
|
||||
"no-empty-source": null
|
||||
}
|
||||
}
|
||||
}
|
||||
6
client/web/discovery/postcss.config.js
Normal file
6
client/web/discovery/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
rtlcss: {},
|
||||
},
|
||||
}
|
||||
1
client/web/discovery/public/.gitignore
vendored
Normal file
1
client/web/discovery/public/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
config.js
|
||||
13
client/web/discovery/public/config.example.js
Normal file
13
client/web/discovery/public/config.example.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Corteza API location
|
||||
window.CortezaAPI = 'https://api.cortezaproject.your-domain.tld'
|
||||
window.CortezaDiscoveryAPI = 'https://api.searcher.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/discovery/public/icon.svg
Normal file
9
client/web/discovery/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/discovery/public/index.html
Normal file
34
client/web/discovery/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="discovery">
|
||||
<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>
|
||||
142
client/web/discovery/src/app.js
Normal file
142
client/web/discovery/src/app.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import './config-check'
|
||||
import './console-splash'
|
||||
|
||||
import './plugins'
|
||||
import './mixins'
|
||||
import './components'
|
||||
import './filters'
|
||||
import store from './store'
|
||||
|
||||
import { i18n, websocket } from '@cortezaproject/corteza-vue'
|
||||
|
||||
import router from './router'
|
||||
|
||||
export default (options = {}) => {
|
||||
options = {
|
||||
el: '#app',
|
||||
name: 'discovery',
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// switch the webapp theme based on user preference
|
||||
if (user.meta.theme) {
|
||||
document.getElementsByTagName('html')[0].setAttribute('data-color-mode', user.meta.theme)
|
||||
}
|
||||
|
||||
// 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-discovery' },
|
||||
'general',
|
||||
'notification',
|
||||
'notifications',
|
||||
'navigation',
|
||||
'filters',
|
||||
'search',
|
||||
),
|
||||
// Any additional options we want to merge
|
||||
...options,
|
||||
}
|
||||
|
||||
const app = new Vue(options)
|
||||
|
||||
// Simple HMR acceptance
|
||||
if (module.hot) {
|
||||
module.hot.accept()
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
108
client/web/discovery/src/components/DiscoveryMap.vue
Normal file
108
client/web/discovery/src/components/DiscoveryMap.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<c-map
|
||||
style="height: calc(100vh - 64px);"
|
||||
:map="{
|
||||
zoom,
|
||||
center,
|
||||
}"
|
||||
:markers="makerValues"
|
||||
@on-marker-click="onMarkerClick"
|
||||
@on-map-click="clearClickedMarker"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CMap } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CMap,
|
||||
},
|
||||
|
||||
props: {
|
||||
markers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
hoverIndex: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
zoom: 3,
|
||||
center: [30, 30],
|
||||
rotation: 0,
|
||||
attribution: '© <a target="_blank" rel="noopener noreferrer" href="http://osm.org/copyright">OpenStreetMap</a>',
|
||||
clickedMarker: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
makerValues () {
|
||||
return this.markers.map((marker, i) => {
|
||||
return {
|
||||
id: i,
|
||||
value: marker.coordinates,
|
||||
opacity: [this.hoverIndex, this.clickedMarker].includes(i) ? 1.0 : 0.6,
|
||||
color: 'var(--primary)',
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
markers: {
|
||||
immediate: true,
|
||||
handler (markers = []) {
|
||||
if (markers.length) {
|
||||
const { coordinates = [30, 30] } = markers[0]
|
||||
this.center = coordinates
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
hoverIndex: {
|
||||
handler (hoverIndex) {
|
||||
if (hoverIndex) {
|
||||
const { coordinates } = this.markers.find(({ id }) => id === hoverIndex) || {}
|
||||
if (coordinates) {
|
||||
this.center = coordinates
|
||||
}
|
||||
}
|
||||
|
||||
this.clickedMarker = undefined
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onMarkerClick ({ index }) {
|
||||
if (index === this.clickedMarker) {
|
||||
this.clickedMarker = undefined
|
||||
} else {
|
||||
this.clickedMarker = index
|
||||
}
|
||||
|
||||
this.$emit('marker-clicked', this.clickedMarker)
|
||||
},
|
||||
|
||||
clearClickedMarker () {
|
||||
this.clickedMarker = undefined
|
||||
this.$emit('marker-clicked', this.clickedMarker)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vl-style-text {
|
||||
color: var(--white);
|
||||
}
|
||||
</style>
|
||||
197
client/web/discovery/src/components/Filters.vue
Normal file
197
client/web/discovery/src/components/Filters.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- <div class="my-2">
|
||||
<h6 class="text-primary mb-2">
|
||||
{{ $t('types.title') }}
|
||||
</h6>
|
||||
<b-form-checkbox-group
|
||||
:checked="storeResourceTypes"
|
||||
name="type"
|
||||
:disabled="storeProcessing"
|
||||
stacked
|
||||
class="mt-1"
|
||||
@change="updateResourceTypes"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
class="pointer ml-2"
|
||||
>
|
||||
<b-avatar
|
||||
:icon="option.icon"
|
||||
size="sm"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
{{ option.text }}
|
||||
</b-form-checkbox>
|
||||
</b-form-checkbox-group>
|
||||
</div> -->
|
||||
|
||||
<div
|
||||
v-for="agg in aggregationOptions"
|
||||
:key="agg.resource"
|
||||
class="my-2"
|
||||
>
|
||||
<div
|
||||
v-if="agg.items.length"
|
||||
class="d-flex justify-content-between align-items-center"
|
||||
style="min-height: 25px;"
|
||||
>
|
||||
<h6
|
||||
class="text-primary d-flex mb-0"
|
||||
>
|
||||
{{ agg.name }}
|
||||
<b-badge
|
||||
v-if="groups[agg.name].length"
|
||||
variant="light"
|
||||
pill
|
||||
class="ml-1 align-self-center"
|
||||
>
|
||||
{{ groups[agg.name].length }}
|
||||
</b-badge>
|
||||
</h6>
|
||||
<b-button
|
||||
v-if="groups[agg.name].length"
|
||||
variant="link"
|
||||
class="text-muted p-0 m-0"
|
||||
size="sm"
|
||||
@click="clearGroup(agg.name)"
|
||||
>
|
||||
{{ $t('reset') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<b-form-checkbox-group
|
||||
v-model="groups[agg.name]"
|
||||
stacked
|
||||
class="mt-1 ml-2"
|
||||
:disabled="storeProcessing"
|
||||
@change="updateGroup(agg.name)"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-for="(resource, i) in agg.items"
|
||||
:key="i"
|
||||
:value="resource.name"
|
||||
class="mb-1"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="d-inline-block text-truncate">
|
||||
{{ getResourceDisplayName(agg.resource, resource) }}
|
||||
</span>
|
||||
|
||||
<!-- <span
|
||||
class="pl-3 ml-auto text-muted"
|
||||
>
|
||||
{{ resource.hits }}
|
||||
</span> -->
|
||||
</div>
|
||||
</b-form-checkbox>
|
||||
</b-form-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
groups: {
|
||||
Module: [],
|
||||
Namespace: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
storeResourceTypes: 'discovery/resourceTypes',
|
||||
storeAggregations: 'discovery/aggregations',
|
||||
storeModules: 'discovery/modules',
|
||||
storeNamespaces: 'discovery/namespaces',
|
||||
storeProcessing: 'discovery/processing',
|
||||
}),
|
||||
|
||||
options () {
|
||||
return [
|
||||
{ text: this.$t('types.namespace'), value: 'compose:namespace', icon: 'code-square' },
|
||||
{ text: this.$t('types.module'), value: 'compose:module', icon: 'list-ul' },
|
||||
{ text: this.$t('types.record'), value: 'compose:record', icon: 'file-earmark-text' },
|
||||
]
|
||||
},
|
||||
|
||||
aggregationOptions () {
|
||||
let namespaceOptions = this.storeAggregations.find(({ resource }) => resource === 'compose:namespace') || {}
|
||||
let moduleOptions = this.storeAggregations.find(({ resource }) => resource === 'compose:module') || {}
|
||||
|
||||
namespaceOptions = {
|
||||
resource: 'compose:namespace',
|
||||
name: 'Namespace',
|
||||
hits: namespaceOptions.hits || 0,
|
||||
items: namespaceOptions.resource_name || [],
|
||||
}
|
||||
|
||||
// Get all modules that are missing from store aggregations but are in filter
|
||||
const missingModuleOptions = this.groups.Module.filter(name => !(moduleOptions.resource_name || []).some(o => o.name === name))
|
||||
.map(name => ({ name }))
|
||||
|
||||
moduleOptions = {
|
||||
resource: 'compose:module',
|
||||
name: 'Module',
|
||||
hits: moduleOptions.hits || 0,
|
||||
items: [
|
||||
...missingModuleOptions,
|
||||
...(moduleOptions.resource_name || []),
|
||||
],
|
||||
}
|
||||
|
||||
return [namespaceOptions, moduleOptions]
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
storeNamespaces: {
|
||||
immediate: true,
|
||||
handler (namespace) {
|
||||
this.groups.Namespace = namespace
|
||||
},
|
||||
},
|
||||
|
||||
storeModules: {
|
||||
immediate: true,
|
||||
handler (module) {
|
||||
this.groups.Module = module
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
updateResourceTypes: 'discovery/updateResourceTypes',
|
||||
}),
|
||||
|
||||
getResourceDisplayName (type, { name, handle, slug }) {
|
||||
if (type === 'compose:namespace') {
|
||||
return name || slug || 'Unnamed namespace'
|
||||
} else if (type === 'compose:module') {
|
||||
return handle || name || 'Unnamed module'
|
||||
}
|
||||
},
|
||||
|
||||
updateGroup (name) {
|
||||
this.$store.dispatch(`discovery/update${name}s`, this.groups[name])
|
||||
},
|
||||
|
||||
clearGroup (name) {
|
||||
this.groups[name] = []
|
||||
this.updateGroup(name)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
90
client/web/discovery/src/components/Results/Module.vue
Normal file
90
client/web/discovery/src/components/Results/Module.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header class="border-bottom">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mb-0"
|
||||
>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
>
|
||||
{{ hit.value.namespace.name || hit.value.namespace.handle }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
class="mx-2"
|
||||
>
|
||||
<b-icon
|
||||
icon="arrow-right"
|
||||
font-scale="1"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
{{ hit.value.name || hit.value.handle }}
|
||||
</span>
|
||||
</h5>
|
||||
|
||||
<span class="text-nowrap">
|
||||
<b-badge
|
||||
v-if="Object.keys(hit.value.labels || { }).includes('federation')"
|
||||
variant="light"
|
||||
class="mr-1 h5 p-2 mb-0"
|
||||
>
|
||||
{{ $t('general:federated') }}
|
||||
</b-badge>
|
||||
<b-avatar
|
||||
size="sm"
|
||||
icon="list-ul"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
{{ $t('types.module') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body
|
||||
class="d-flex flex-wrap"
|
||||
style="gap: 2rem;"
|
||||
>
|
||||
<b-form-group
|
||||
v-for="(value, name, i) in limitData()"
|
||||
:key="i"
|
||||
:label="name"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 20rem; max-width: 100%;"
|
||||
>
|
||||
{{ value }}
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'fields',
|
||||
'namespace',
|
||||
'labels',
|
||||
'module',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
65
client/web/discovery/src/components/Results/Namespace.vue
Normal file
65
client/web/discovery/src/components/Results/Namespace.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header class="border-bottom">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mb-0"
|
||||
>
|
||||
{{ hit.value.name || hit.value.slug }}
|
||||
</h5>
|
||||
|
||||
<span class="text-nowrap">
|
||||
<b-avatar
|
||||
size="sm"
|
||||
icon="code-square"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
{{ $t('types.namespace') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body>
|
||||
<div
|
||||
class="d-flex flex-wrap"
|
||||
style="gap: 2rem;"
|
||||
>
|
||||
<b-form-group
|
||||
v-for="(item, name, i) in limitData()"
|
||||
:key="i"
|
||||
:label="name"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 20rem; max-width: 100%;"
|
||||
>
|
||||
{{ item }}
|
||||
</b-form-group>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'namespace',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
176
client/web/discovery/src/components/Results/Record.vue
Normal file
176
client/web/discovery/src/components/Results/Record.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header class="border-bottom">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<h5 class="text-primary text-capitalize text-truncate mb-0">
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
>
|
||||
{{ hit.value.namespace.name || hit.value.namespace.handle }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
class="mx-2"
|
||||
>
|
||||
<b-icon
|
||||
icon="arrow-right"
|
||||
font-scale="1"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
{{ hit.value.module.name || hit.value.module.handle }}
|
||||
</span>
|
||||
</h5>
|
||||
|
||||
<span class="text-nowrap">
|
||||
<b-avatar
|
||||
size="sm"
|
||||
icon="file-earmark-text"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
{{ $t('types.record') }}
|
||||
</span>
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body class="d-flex flex-column flex-wrap gap-2">
|
||||
<div v-if="Object.keys(hit.value.labels || { }).includes('federation')">
|
||||
<b-badge
|
||||
variant="light"
|
||||
class="h6 mb-0"
|
||||
>
|
||||
{{ $t('general:federated') }}
|
||||
</b-badge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="recordValues.length"
|
||||
class="d-flex flex-wrap gap-2 flex-grow-1"
|
||||
>
|
||||
<b-form-group
|
||||
v-for="(item, i) in recordValues"
|
||||
:key="i"
|
||||
:label="item.label || item.name"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 20rem; max-width: 100%; white-space: pre-line;"
|
||||
>
|
||||
{{ item.value }}
|
||||
</b-form-group>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="systemValues.length"
|
||||
class="d-flex flex-wrap gap-2 flex-grow-1"
|
||||
>
|
||||
<b-form-group
|
||||
v-for="item in systemValues"
|
||||
:key="item.name"
|
||||
:label="item.label"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 20rem; max-width: 100%;"
|
||||
>
|
||||
<c-user-label
|
||||
v-if="item.name === 'createdBy'"
|
||||
:user="createdByUser"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ item.value }}
|
||||
</template>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import base from './base'
|
||||
|
||||
const { CUserLabel } = components
|
||||
|
||||
const userCache = new Map()
|
||||
const inflight = new Map()
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
components: {
|
||||
CUserLabel,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
enrichedUser: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
recordID () {
|
||||
return this.hit.value.recordID
|
||||
},
|
||||
|
||||
recordValues () {
|
||||
const { values = [] } = this.hit.value
|
||||
|
||||
return (values || []).map(({ name, label, value = [] }) => {
|
||||
if (value) {
|
||||
value = value.map(v => {
|
||||
return (v !== null ? v : '').toString().includes('{"coordinates":[') ? ((JSON.parse(v || '{}') || {}).coordinates || []).join(', ') : v
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
return { name, label, value }
|
||||
})
|
||||
},
|
||||
|
||||
createdByUser () {
|
||||
return this.enrichedUser || this.createdBy || {}
|
||||
},
|
||||
|
||||
systemValues () {
|
||||
return [
|
||||
{ name: 'recordID', label: this.$t('general:recordID'), value: this.recordID },
|
||||
{ name: 'createdBy', label: this.$t('general:createdBy'), value: this.createdBy },
|
||||
{ name: 'createdAt', label: this.$t('general:createdAt'), value: this.createdAt },
|
||||
{ name: 'updatedAt', label: this.$t('general:updatedAt'), value: this.updatedAt },
|
||||
].filter(v => v.value)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'createdBy.userID': {
|
||||
immediate: true,
|
||||
handler (userID) {
|
||||
if (!userID) {
|
||||
this.enrichedUser = null
|
||||
return
|
||||
}
|
||||
if (userCache.has(userID)) {
|
||||
this.enrichedUser = userCache.get(userID)
|
||||
return
|
||||
}
|
||||
const pending = inflight.get(userID) || this.$SystemAPI.userRead({ userID })
|
||||
.then(user => {
|
||||
userCache.set(userID, user)
|
||||
return user
|
||||
})
|
||||
.finally(() => {
|
||||
inflight.delete(userID)
|
||||
})
|
||||
inflight.set(userID, pending)
|
||||
pending.then(user => {
|
||||
this.enrichedUser = user
|
||||
}).catch(() => {
|
||||
// keep snapshot fallback
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
60
client/web/discovery/src/components/Results/User.vue
Normal file
60
client/web/discovery/src/components/Results/User.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header class="border-bottom">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mr-2 mb-0"
|
||||
>
|
||||
{{ $t('types.user') }}
|
||||
</h5>
|
||||
|
||||
<b-avatar
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('types.user'), boundary: 'body' }"
|
||||
size="sm"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body class="pb-0">
|
||||
<b-form-group
|
||||
v-for="(value, name, i) in limitData()"
|
||||
:key="i"
|
||||
:label="name"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 200px; max-width: 100%;"
|
||||
>
|
||||
{{ value }}
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'fields',
|
||||
'namespace',
|
||||
'labels',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
79
client/web/discovery/src/components/Results/base.vue
Normal file
79
client/web/discovery/src/components/Results/base.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showMap: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
defaultBlacklistedFields: ['deleted', 'created', 'updated', 'security', 'catch_all'],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return this.defaultBlacklistedFields
|
||||
},
|
||||
|
||||
query () {
|
||||
return [this.$route.query.query || '']
|
||||
},
|
||||
|
||||
createdBy () {
|
||||
const { by } = this.hit.value.created || {}
|
||||
return by
|
||||
},
|
||||
|
||||
createdAt () {
|
||||
const { at } = this.hit.value.created || {}
|
||||
return at ? this.$options.filters.locFullDateTime(at) : at
|
||||
},
|
||||
|
||||
updatedAt () {
|
||||
const { at } = this.hit.value.updated || {}
|
||||
return at ? this.$options.filters.locFullDateTime(at) : at
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
limitData () {
|
||||
const out = {}
|
||||
|
||||
if (this.hit.value) {
|
||||
for (const key in this.hit.value) {
|
||||
const value = this.hit.value[key]
|
||||
|
||||
if (!!value && this.blacklistedFields.indexOf(key) < 0) {
|
||||
out[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.createdBy) {
|
||||
out.createdBy = this.createdBy
|
||||
}
|
||||
|
||||
if (this.createdAt) {
|
||||
out.createdAt = this.createdAt
|
||||
}
|
||||
|
||||
if (this.updatedAt) {
|
||||
out.updatedAt = this.updatedAt
|
||||
}
|
||||
|
||||
return out
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
74
client/web/discovery/src/components/Results/index.vue
Normal file
74
client/web/discovery/src/components/Results/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<b-card
|
||||
no-body
|
||||
class="result shadow-sm h-100"
|
||||
:class="{ 'shadow': hovered }"
|
||||
@mouseover="onHover"
|
||||
@mouseleave="onLeave"
|
||||
>
|
||||
<a
|
||||
v-if="hit.value.url"
|
||||
:href="hit.value.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="stretched-link"
|
||||
/>
|
||||
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="$props"
|
||||
/>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import * as Results from './loader'
|
||||
|
||||
export default {
|
||||
extends: base,
|
||||
|
||||
props: {
|
||||
hit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
hovered: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
component () {
|
||||
const { type } = this.hit
|
||||
const resourceType = type.split(':')[1]
|
||||
|
||||
const keys = Object.keys(Results)
|
||||
const i = keys.map(c => c.toLocaleLowerCase()).findIndex(c => c === resourceType)
|
||||
|
||||
return Results[keys[i]]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onHover () {
|
||||
this.hovered = true
|
||||
this.$emit('hover', this.hit.value.recordID)
|
||||
},
|
||||
|
||||
onLeave () {
|
||||
this.hovered = false
|
||||
this.$emit('hover', undefined)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.result {
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
</style>
|
||||
4
client/web/discovery/src/components/Results/loader.js
Normal file
4
client/web/discovery/src/components/Results/loader.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Module } from './Module'
|
||||
export { default as Namespace } from './Namespace'
|
||||
export { default as Record } from './Record'
|
||||
export { default as User } from './User'
|
||||
445
client/web/discovery/src/components/Search.vue
Normal file
445
client/web/discovery/src/components/Search.vue
Normal file
@@ -0,0 +1,445 @@
|
||||
<template>
|
||||
<b-container
|
||||
fluid
|
||||
class="h-100 mh-100 p-0 d-flex flex-column"
|
||||
>
|
||||
<split
|
||||
direction="horizontal"
|
||||
:gutter-size="12"
|
||||
class="h-100 overflow-hidden"
|
||||
>
|
||||
<split-area
|
||||
:size="map.show ? 70 : 100"
|
||||
:min-size="300"
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<div class="px-3 flex-shrink-0">
|
||||
<b-form-group class="mb-0">
|
||||
<c-input-search
|
||||
:value="query"
|
||||
:placeholder="$t('input-placeholder')"
|
||||
:autocomplete="'off'"
|
||||
:disabled="storeProcessing"
|
||||
submittable
|
||||
@search="onQuerySubmit"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<div
|
||||
class="d-flex align-items-center px-1 mt-1 mb-2 text-muted"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<span
|
||||
:class="{ 'discovering': storeProcessing }"
|
||||
class="mt-1"
|
||||
>
|
||||
{{ searchDescription }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center ml-auto gap-2">
|
||||
<b-button
|
||||
variant="extra-light"
|
||||
size="sm"
|
||||
class="d-flex align-items-center gap-1 mt-2"
|
||||
@click="toggleMap"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'map-marked-alt']"
|
||||
/>
|
||||
{{ !map.show ? $t('search:show-map') : $t('search:hide-map') }}
|
||||
</b-button>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'grip-lines']"
|
||||
class="mt-2 mr-1 pointer"
|
||||
:class="{ 'text-primary': viewMode === 'list' }"
|
||||
@click="viewMode = 'list'"
|
||||
/>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="viewMode"
|
||||
:value="'grid'"
|
||||
:unchecked-value="'list'"
|
||||
switch
|
||||
class="pointer ml-2"
|
||||
/>
|
||||
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'grip-horizontal']"
|
||||
class="mt-2 ml-1 pointer"
|
||||
:class="{ 'text-primary': viewMode === 'grid' }"
|
||||
@click="viewMode = 'grid'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-fill overflow-hidden">
|
||||
<div
|
||||
v-if="(storeProcessing || !total.actual) && !loadingMore"
|
||||
class="d-flex align-items-center justify-content-center w-100 my-5"
|
||||
>
|
||||
<h5 class="mb-0">
|
||||
<b-spinner
|
||||
v-if="storeProcessing"
|
||||
variant="primary"
|
||||
class="p-4"
|
||||
/>
|
||||
<span
|
||||
v-else-if="!total.actual"
|
||||
>
|
||||
{{ $t('no-results') }}
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="results d-flex flex-wrap gap-3 px-4 py-3 overflow-auto"
|
||||
:class="{ 'list-view': viewMode === 'list' }"
|
||||
>
|
||||
<div
|
||||
v-for="(hit, i) in hits"
|
||||
:key="i"
|
||||
class="result-item w-100"
|
||||
:class="{ 'grid-view': viewMode === 'grid' }"
|
||||
>
|
||||
<result
|
||||
:id="`result-${i}`"
|
||||
:index="i"
|
||||
:hit="hit"
|
||||
:show-map="map.show"
|
||||
:class="{ 'border-primary border shadow': map.clickedMarker && map.clickedMarker === i }"
|
||||
class="border"
|
||||
@hover="onResultHover(i)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="total.actual > 0 && total.actual < total.all"
|
||||
class="d-flex align-items-center justify-content-center py-3 w-100"
|
||||
>
|
||||
<b-button
|
||||
variant="primary"
|
||||
:disabled="loadingMore"
|
||||
class="d-flex align-items-center justify-content-center gap-1"
|
||||
@click="getSearchData({ append: true })"
|
||||
>
|
||||
<b-spinner
|
||||
v-if="loadingMore"
|
||||
small
|
||||
/>
|
||||
{{ loadingMore ? $t('search:loading-more') : $t('search:show-more') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</split-area>
|
||||
|
||||
<split-area
|
||||
:size="map.show ? 30 : 0"
|
||||
:min-size="300"
|
||||
>
|
||||
<discovery-map
|
||||
v-if="map.show"
|
||||
:markers="map.markers"
|
||||
:hover-index="map.hoverIndex"
|
||||
class="pl-2"
|
||||
@marker-clicked="markerClicked"
|
||||
/>
|
||||
</split-area>
|
||||
</split>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import Result from './Results'
|
||||
import DiscoveryMap from './DiscoveryMap.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import { Split, SplitArea } from 'vue-split-panel'
|
||||
const { CInputSearch } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'search',
|
||||
},
|
||||
|
||||
components: {
|
||||
Result,
|
||||
DiscoveryMap,
|
||||
CInputSearch,
|
||||
Split,
|
||||
SplitArea,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
loadingMore: false,
|
||||
|
||||
query: '',
|
||||
|
||||
hits: [],
|
||||
|
||||
pagination: {
|
||||
limit: 50,
|
||||
from: 0,
|
||||
size: 50,
|
||||
},
|
||||
|
||||
total: {
|
||||
all: 0,
|
||||
actual: 0,
|
||||
},
|
||||
|
||||
initial: false,
|
||||
|
||||
map: {
|
||||
show: false,
|
||||
markers: [],
|
||||
clickedMarker: undefined,
|
||||
hoverIndex: undefined,
|
||||
},
|
||||
|
||||
viewMode: 'list',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
storeProcessing: 'discovery/processing',
|
||||
storeResourceTypes: 'discovery/resourceTypes',
|
||||
storeModules: 'discovery/modules',
|
||||
storeNamespaces: 'discovery/namespaces',
|
||||
}),
|
||||
|
||||
searchDescription () {
|
||||
if (this.storeProcessing) {
|
||||
return this.$t('discovering')
|
||||
}
|
||||
|
||||
if (this.total.all > 0) {
|
||||
return this.$t('range', { actual: this.total.actual, all: this.total.all })
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
storeResourceTypes: {
|
||||
handler () {
|
||||
if (this.initial) return
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData()
|
||||
},
|
||||
},
|
||||
|
||||
storeModules: {
|
||||
handler () {
|
||||
if (this.initial) return
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData()
|
||||
},
|
||||
},
|
||||
|
||||
storeNamespaces: {
|
||||
handler () {
|
||||
if (this.initial) return
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.initial = true
|
||||
|
||||
const { query = '', modules, namespaces, resourceTypes, size = 50 } = this.$route.query
|
||||
|
||||
this.query = query
|
||||
this.pagination.size = size
|
||||
|
||||
if (namespaces) {
|
||||
this.updateNamespaces(Array.isArray(namespaces) ? namespaces : [namespaces])
|
||||
}
|
||||
|
||||
if (modules) {
|
||||
this.updateModules(Array.isArray(modules) ? modules : [modules])
|
||||
}
|
||||
|
||||
if (resourceTypes) {
|
||||
this.updateResourceTypes(Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes])
|
||||
}
|
||||
|
||||
this.getSearchData()
|
||||
|
||||
setTimeout(() => {
|
||||
this.initial = false
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
fetchData: 'discovery/fetchData',
|
||||
updateModules: 'discovery/updateModules',
|
||||
updateNamespaces: 'discovery/updateNamespaces',
|
||||
updateResourceTypes: 'discovery/updateResourceTypes',
|
||||
}),
|
||||
|
||||
getSearchData ({ query = this.query, append = false } = {}) {
|
||||
if (append) {
|
||||
this.pagination.size += this.pagination.limit
|
||||
this.loadingMore = true
|
||||
} else {
|
||||
this.map.markers = []
|
||||
this.hits = []
|
||||
this.map.hoverIndex = undefined
|
||||
}
|
||||
|
||||
const modules = this.storeModules
|
||||
const namespaces = this.storeNamespaces
|
||||
|
||||
const { size } = this.pagination
|
||||
|
||||
this.updateRouteQuery({ query, modules, namespaces, size })
|
||||
|
||||
this.fetchData({ query, modules, namespaces, size }).then((response = {}) => {
|
||||
if (response) {
|
||||
if (append) {
|
||||
this.hits = [...this.hits, ...(response.hits || [])]
|
||||
} else {
|
||||
this.hits = response.hits || []
|
||||
}
|
||||
this.total.all = response.total_results || 0
|
||||
this.total.actual = this.hits.length
|
||||
|
||||
this.pagination = {
|
||||
...this.pagination,
|
||||
from: response.from || 0,
|
||||
size: response.size || 0,
|
||||
}
|
||||
|
||||
this.getMarkers()
|
||||
}
|
||||
}).catch(e => {
|
||||
this.toastErrorHandler(this.$t('notification:search.failed'))(e)
|
||||
this.hits = []
|
||||
}).finally(() => {
|
||||
this.loadingMore = false
|
||||
})
|
||||
},
|
||||
|
||||
onQuerySubmit (query) {
|
||||
if (!this.storeProcessing) {
|
||||
this.query = query
|
||||
this.pagination.size = 50
|
||||
this.getSearchData()
|
||||
}
|
||||
},
|
||||
|
||||
getMarkers () {
|
||||
const markers = []
|
||||
|
||||
this.hits.forEach(({ type, value }) => {
|
||||
if (type === 'compose:record' && Array.isArray(value.values)) {
|
||||
const id = value.recordID
|
||||
value.values.forEach(({ value = [] }) => {
|
||||
const isGeometry = value && value.find(v => {
|
||||
return (v !== null ? v : '').toString().includes('{"coordinates":[')
|
||||
})
|
||||
|
||||
if (isGeometry) {
|
||||
value.forEach(coordinates => {
|
||||
coordinates = JSON.parse(coordinates || '{}').coordinates
|
||||
if (coordinates && coordinates.length) {
|
||||
markers.push({ id, coordinates })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.map.markers = markers
|
||||
},
|
||||
|
||||
markerClicked (ID) {
|
||||
if (ID) {
|
||||
const result = document.getElementById(`result-${ID}`)
|
||||
|
||||
if (result) {
|
||||
result.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.map.clickedMarker = ID
|
||||
this.map.hoverIndex = undefined
|
||||
},
|
||||
|
||||
onResultHover (index) {
|
||||
if (this.map.show) {
|
||||
this.map.hoverIndex = index
|
||||
}
|
||||
},
|
||||
|
||||
toggleMap () {
|
||||
this.map.show = !this.map.show
|
||||
},
|
||||
|
||||
updateRouteQuery ({ query = undefined, modules = [], namespaces = [], size = 0 }) {
|
||||
if (JSON.stringify(this.$route.query) !== JSON.stringify({ query, modules, namespaces, size })) {
|
||||
this.$router.push({ query: { query: query || undefined, modules, namespaces, size } })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.split .gutter {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// https://stackoverflow.com/a/40991531/17926309
|
||||
.discovering::after {
|
||||
display: inline-block;
|
||||
animation: discovering steps(1, end) 1s infinite;
|
||||
content: '';
|
||||
}
|
||||
|
||||
@keyframes discovering {
|
||||
0% { content: ''; }
|
||||
25% { content: '.'; }
|
||||
50% { content: '..'; }
|
||||
75% { content: '...'; }
|
||||
100% { content: ''; }
|
||||
}
|
||||
|
||||
.result-item {
|
||||
&.grid-view {
|
||||
min-width: 30rem;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-view {
|
||||
.result-item {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
56
client/web/discovery/src/components/faIcons.js
Normal file
56
client/web/discovery/src/components/faIcons.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGripHorizontal,
|
||||
faGripLines,
|
||||
faThumbtack,
|
||||
faBars,
|
||||
faChevronUp,
|
||||
faChevronDown,
|
||||
faSearch,
|
||||
faMapMarkedAlt,
|
||||
faTimes,
|
||||
faLocationArrow,
|
||||
faCheckDouble,
|
||||
faBellSlash,
|
||||
faBell as faBellSolid,
|
||||
faEllipsisV,
|
||||
faCheck,
|
||||
faLocationCrosshairs,
|
||||
faPaperPlane,
|
||||
faRobot,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faQuestionCircle,
|
||||
faUser,
|
||||
faBell,
|
||||
faTrashAlt,
|
||||
faEnvelope,
|
||||
faEnvelopeOpen,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faUser,
|
||||
faQuestionCircle,
|
||||
faGripHorizontal,
|
||||
faThumbtack,
|
||||
faBars,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faSearch,
|
||||
faMapMarkedAlt,
|
||||
faTimes,
|
||||
faLocationArrow,
|
||||
faCheckDouble,
|
||||
faBell,
|
||||
faBellSolid,
|
||||
faBellSlash,
|
||||
faEllipsisV,
|
||||
faTrashAlt,
|
||||
faGripLines,
|
||||
faCheck,
|
||||
faLocationCrosshairs,
|
||||
faEnvelope,
|
||||
faEnvelopeOpen,
|
||||
faPaperPlane,
|
||||
faRobot,
|
||||
)
|
||||
15
client/web/discovery/src/components/index.js
Normal file
15
client/web/discovery/src/components/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import PortalVue from 'portal-vue'
|
||||
import './faIcons'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
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-input-processing', components.CInputProcessing)
|
||||
Vue.component('c-input-search', components.CInputSearch)
|
||||
8
client/web/discovery/src/config-check.js
Normal file
8
client/web/discovery/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/discovery/src/console-splash.js
Normal file
6
client/web/discovery/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)
|
||||
6
client/web/discovery/src/filters/index.js
Normal file
6
client/web/discovery/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])
|
||||
}
|
||||
5
client/web/discovery/src/main.js
Normal file
5
client/web/discovery/src/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import './themes'
|
||||
import app from './app'
|
||||
|
||||
// Mount the app
|
||||
app()
|
||||
5
client/web/discovery/src/mixins/index.js
Normal file
5
client/web/discovery/src/mixins/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import { mixins } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.mixin(mixins.toast)
|
||||
30
client/web/discovery/src/plugins/index.js
Normal file
30
client/web/discovery/src/plugins/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import Router from 'vue-router'
|
||||
import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
|
||||
import VueNativeSock from 'vue-native-websocket'
|
||||
import VueSimpleMarkdown from 'vue-simple-markdown'
|
||||
import 'vue-simple-markdown/dist/vue-simple-markdown.css'
|
||||
import { plugins, websocket } from '@cortezaproject/corteza-vue'
|
||||
import DiscoveryAPI from './searcher.js'
|
||||
|
||||
Vue.use(Vuex)
|
||||
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: 'discovery' })
|
||||
Vue.use(plugins.CortezaAPI('system'))
|
||||
Vue.use(plugins.CortezaAPI('compose'))
|
||||
Vue.use(plugins.Settings, { api: Vue.prototype.$SystemAPI })
|
||||
Vue.use(DiscoveryAPI())
|
||||
Vue.use(VueNativeSock, websocket.endpoint(), websocket.config)
|
||||
Vue.use(VueSimpleMarkdown)
|
||||
130
client/web/discovery/src/plugins/searcher.js
Normal file
130
client/web/discovery/src/plugins/searcher.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export default function (service, opt = {}) {
|
||||
if (!opt.baseURL) {
|
||||
// @ts-ignore
|
||||
if (!window.CortezaDiscoveryAPI) {
|
||||
throw new Error('config.js missing or window.CortezaDiscoveryAPI not set')
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
opt.baseURL = `${window.CortezaDiscoveryAPI}/`
|
||||
}
|
||||
|
||||
return function (Vue) {
|
||||
if (!opt.accessTokenFn) {
|
||||
/**
|
||||
* Checking if auth plugin was initialized before and
|
||||
* hooking on to it's accessTokenFn
|
||||
*/
|
||||
opt.accessTokenFn = Vue.prototype.$auth.accessTokenFn
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// makes Vue.$<service>API (Vue.$SystemAPI, Vue.$ComposeAPI, Vue.$FederationAPI, Vue.$AutomationAPI) available
|
||||
Vue.prototype.$DiscoveryAPI = new Searcher(opt)
|
||||
}
|
||||
}
|
||||
|
||||
function stdResolve (response) {
|
||||
if (response.data.error) {
|
||||
return Promise.reject(response.data.error)
|
||||
} else {
|
||||
return response.data.response
|
||||
}
|
||||
}
|
||||
|
||||
class Searcher {
|
||||
baseURL
|
||||
accessTokenFn
|
||||
headers = {}
|
||||
|
||||
constructor ({ baseURL, headers, accessTokenFn }) {
|
||||
this.baseURL = baseURL
|
||||
this.accessTokenFn = accessTokenFn
|
||||
this.headers = {
|
||||
/**
|
||||
* All we send is JSON
|
||||
*/
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
this.setHeaders(headers)
|
||||
}
|
||||
|
||||
setAccessTokenFn (fn) {
|
||||
this.accessTokenFn = fn
|
||||
return this
|
||||
}
|
||||
|
||||
setHeaders (headers) {
|
||||
if (typeof headers === 'object') {
|
||||
this.headers = headers
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setHeader (name, value) {
|
||||
if (value === undefined) {
|
||||
delete this.headers[name]
|
||||
} else {
|
||||
this.headers[name] = value
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
api () {
|
||||
const headers = { ...this.headers }
|
||||
const accessToken = this.accessTokenFn ? this.accessTokenFn() : undefined
|
||||
if (accessToken) {
|
||||
headers.Authorization = 'Bearer ' + accessToken
|
||||
}
|
||||
|
||||
return axios.create({
|
||||
withCredentials: true,
|
||||
baseURL: this.baseURL,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
// List namespaces
|
||||
async query (a, extra = {}) {
|
||||
const {
|
||||
modules,
|
||||
namespaces,
|
||||
from,
|
||||
size,
|
||||
resourceTypes,
|
||||
} = a || {}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
// Filter
|
||||
if (modules?.length > 0) modules.forEach(m => params.append('moduleAggs', m))
|
||||
if (namespaces?.length > 0) namespaces.forEach(n => params.append('namespaceAggs', n))
|
||||
if (resourceTypes?.length > 0) resourceTypes.forEach(t => params.append('resourceTypes', t))
|
||||
|
||||
// Pagination
|
||||
if (from) params.append('from', from)
|
||||
if (size) params.append('size', size)
|
||||
|
||||
const cfg = {
|
||||
...extra,
|
||||
method: 'get',
|
||||
url: this.queryEndpoint(a),
|
||||
params,
|
||||
}
|
||||
|
||||
return this.api().request(cfg).then(result => stdResolve(result))
|
||||
}
|
||||
|
||||
queryEndpoint (a) {
|
||||
const {
|
||||
query = '',
|
||||
} = a || {}
|
||||
|
||||
return `/?q=${query}`
|
||||
}
|
||||
}
|
||||
7
client/web/discovery/src/router.js
Normal file
7
client/web/discovery/src/router.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Router from 'vue-router'
|
||||
import routes from './views/routes'
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
20
client/web/discovery/src/router/index.js
Normal file
20
client/web/discovery/src/router/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
component: () => import('../views/Layout'),
|
||||
},
|
||||
{ path: '*', redirect: { name: 'root' } },
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
81
client/web/discovery/src/store/discovery.js
Normal file
81
client/web/discovery/src/store/discovery.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const storeTypes = {
|
||||
processing: 'processing',
|
||||
resourceTypes: 'resourceTypes',
|
||||
aggregations: 'aggregations',
|
||||
modules: 'modules',
|
||||
namespaces: 'namespaces',
|
||||
}
|
||||
|
||||
export default function (DiscoveryAPI) {
|
||||
return {
|
||||
namespaced: true,
|
||||
|
||||
state: {
|
||||
processing: false,
|
||||
resourceTypes: [
|
||||
'compose:record',
|
||||
],
|
||||
aggregations: [],
|
||||
modules: [],
|
||||
namespaces: [],
|
||||
},
|
||||
|
||||
getters: {
|
||||
processing: state => state.processing,
|
||||
resourceTypes: state => state.resourceTypes,
|
||||
aggregations: state => state.aggregations,
|
||||
modules: state => state.modules,
|
||||
namespaces: state => state.namespaces,
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchData ({ commit, state }, { query, modules = state.modules, namespaces = state.namespaces, size }) {
|
||||
commit(storeTypes.processing, true)
|
||||
|
||||
return DiscoveryAPI.query({ query, modules, namespaces, size, resourceTypes: state.resourceTypes }).then((response = {}) => {
|
||||
if (response) {
|
||||
commit(storeTypes.aggregations, response.aggregations)
|
||||
}
|
||||
|
||||
return response
|
||||
}).finally(() => {
|
||||
commit(storeTypes.processing, false)
|
||||
})
|
||||
},
|
||||
|
||||
updateResourceTypes ({ commit }, resourceTypes) {
|
||||
commit(storeTypes.resourceTypes, resourceTypes)
|
||||
},
|
||||
|
||||
updateModules ({ commit }, modules) {
|
||||
commit(storeTypes.modules, modules)
|
||||
},
|
||||
|
||||
updateNamespaces ({ commit }, namespaces) {
|
||||
commit(storeTypes.namespaces, namespaces)
|
||||
},
|
||||
},
|
||||
|
||||
mutations: {
|
||||
[storeTypes.processing] (state, value) {
|
||||
state.processing = value
|
||||
},
|
||||
|
||||
[storeTypes.resourceTypes] (state, resourceTypes) {
|
||||
state.resourceTypes = resourceTypes
|
||||
},
|
||||
|
||||
[storeTypes.aggregations] (state, aggregations) {
|
||||
state.aggregations = aggregations
|
||||
},
|
||||
|
||||
[storeTypes.modules] (state, modules) {
|
||||
state.modules = modules
|
||||
},
|
||||
|
||||
[storeTypes.namespaces] (state, namespaces) {
|
||||
state.namespaces = namespaces
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
21
client/web/discovery/src/store/index.js
Normal file
21
client/web/discovery/src/store/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import discovery from './discovery'
|
||||
import { store as cvStore } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
strict: process.env.NODE_ENV !== 'production',
|
||||
|
||||
modules: {
|
||||
discovery: discovery(Vue.prototype.$DiscoveryAPI),
|
||||
notifications: {
|
||||
namespaced: true,
|
||||
...cvStore.notifications({
|
||||
api: Vue.prototype.$SystemAPI,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
5
client/web/discovery/src/themes/corteza-base/custom.scss
Normal file
5
client/web/discovery/src/themes/corteza-base/custom.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.custom-control-label {
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
1
client/web/discovery/src/themes/corteza-base/index.js
Normal file
1
client/web/discovery/src/themes/corteza-base/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import './index.scss'
|
||||
2
client/web/discovery/src/themes/corteza-base/index.scss
Normal file
2
client/web/discovery/src/themes/corteza-base/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "./custom";
|
||||
@import "./poppins.scss";
|
||||
146
client/web/discovery/src/themes/corteza-base/poppins.scss
Normal file
146
client/web/discovery/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/discovery/src/themes/index.js
Normal file
2
client/web/discovery/src/themes/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
import(/* webpackChunkName: 'corteza-base' */ './corteza-base')
|
||||
126
client/web/discovery/src/views/Layout.vue
Normal file
126
client/web/discovery/src/views/Layout.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<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('appMenu'),
|
||||
helpForum: $t('help.forum'),
|
||||
helpDocumentation: $t('help.documentation'),
|
||||
helpFeedback: $t('help.feedback'),
|
||||
helpVersion: $t('help.version'),
|
||||
userSettingsLoggedInAs: $t('userSettings.loggedInAs', { user }),
|
||||
userSettingsProfile: $t('userSettings.profile'),
|
||||
userSettingsChangePassword: $t('userSettings.changePassword'),
|
||||
userSettingsLogout: $t('userSettings.logout'),
|
||||
userSettingsTheme: $t('userSettings.theme'),
|
||||
lightTheme: $t('general:themes.labels.light'),
|
||||
darkTheme: $t('general:themes.labels.dark'),
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
{{ $t("discovery") }}
|
||||
</template>
|
||||
</c-topbar>
|
||||
</header>
|
||||
|
||||
<aside>
|
||||
<c-sidebar
|
||||
:expanded.sync="expanded"
|
||||
:icon="icon"
|
||||
:logo="logo"
|
||||
expand-on-click
|
||||
>
|
||||
<template #body-expanded>
|
||||
<filters />
|
||||
</template>
|
||||
</c-sidebar>
|
||||
</aside>
|
||||
|
||||
<main class="d-inline-flex overflow-hidden">
|
||||
<!--
|
||||
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="flex-grow-1 overflow-hidden">
|
||||
<search />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<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 Search from '../components/Search.vue'
|
||||
import Filters from '../components/Filters.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CTopbar, CSidebar, CExtendSession, CNotificationSidebar } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'navigation',
|
||||
},
|
||||
|
||||
components: {
|
||||
CTopbar,
|
||||
CSidebar,
|
||||
Search,
|
||||
Filters,
|
||||
CExtendSession,
|
||||
CNotificationSidebar,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
expanded: false,
|
||||
}
|
||||
},
|
||||
|
||||
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')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.h-viewport {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
}
|
||||
</style>
|
||||
10
client/web/discovery/src/views/routes.js
Normal file
10
client/web/discovery/src/views/routes.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default [
|
||||
{
|
||||
name: 'root',
|
||||
path: '',
|
||||
component: () => import('./Layout.vue'),
|
||||
},
|
||||
|
||||
// When everything else fails, go to root
|
||||
{ path: '*', redirect: { name: 'root' } },
|
||||
]
|
||||
31
client/web/discovery/tests/setup.js
Normal file
31
client/web/discovery/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/discovery/tests/unit/.eslintrc.js
Normal file
7
client/web/discovery/tests/unit/.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true,
|
||||
},
|
||||
}
|
||||
78
client/web/discovery/tests/unit/views/layout.spec.js
Normal file
78
client/web/discovery/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/discovery/vue.config-builder.js
Normal file
239
client/web/discovery/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/discovery/vue.config.js
Normal file
9
client/web/discovery/vue.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const buildVueConfig = require('./vue.config-builder')
|
||||
|
||||
module.exports = buildVueConfig({
|
||||
appFlavour: 'Discovery',
|
||||
appName: 'discovery',
|
||||
appLabel: 'Corteza Discovery Discovery',
|
||||
theme: 'corteza-base',
|
||||
packageAlias: 'corteza-webapp-discovery',
|
||||
})
|
||||
Reference in New Issue
Block a user