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:
47
client/Makefile
Normal file
47
client/Makefile
Normal file
@@ -0,0 +1,47 @@
|
||||
WEB_SUBDIRS := $(filter %/, $(wildcard $(CURDIR)/web/*/))
|
||||
|
||||
.PHONY: dev test lint fresh build audit
|
||||
|
||||
yarn:
|
||||
@for dir in $(WEB_SUBDIRS); do \
|
||||
echo "---Yarning $$dir---"; \
|
||||
(cd $$dir && yarn) || (echo "Failed to yarn $$dir"; exit 1); \
|
||||
done
|
||||
|
||||
dev:
|
||||
@for dir in $(WEB_SUBDIRS); do \
|
||||
echo "---Installing and linking clients $$dir---"; \
|
||||
(cd $$dir && yarn && yarn cdeps) || (echo "Failed to run yarn or link libs $$dir"; exit 1); \
|
||||
done
|
||||
|
||||
test:
|
||||
@for dir in $(WEB_SUBDIRS); do \
|
||||
echo "---Testing $$dir---"; \
|
||||
(cd $$dir && yarn test) || (echo "Failed to test $$dir"; exit 1); \
|
||||
done
|
||||
|
||||
lint:
|
||||
@for dir in $(WEB_SUBDIRS); do \
|
||||
echo "---Linting $$dir---"; \
|
||||
(cd $$dir && yarn lint) || (echo "Failed to lint $$dir"; exit 1); \
|
||||
done
|
||||
|
||||
fresh:
|
||||
@for dir in $(WEB_SUBDIRS); do \
|
||||
echo "---Fresh $$dir---"; \
|
||||
(cd $$dir && rm -rf node_modules yarn.lock && yarn && yarn cdeps) || (echo "Failed to fresh $$dir"; exit 1); \
|
||||
done
|
||||
|
||||
build:
|
||||
@for dir in $(WEB_SUBDIRS); do \
|
||||
echo "---Building $$dir---"; \
|
||||
(cd $$dir && yarn build) || (echo "Failed to build $$dir"; exit 1); \
|
||||
done
|
||||
|
||||
audit:
|
||||
@for dir in $(WEB_SUBDIRS); do \
|
||||
echo "---Auditing $$dir---"; \
|
||||
(cd $$dir && yarn audit) || true; \
|
||||
done
|
||||
|
||||
.DEFAULT_GOAL := dev
|
||||
36
client/web/Dockerfile
Normal file
36
client/web/Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
||||
# build-stage
|
||||
FROM alpine:3 as build-stage
|
||||
|
||||
ARG VERSION=2022.9.0
|
||||
ARG CORTEZA_CLIENT_WEB_AIO_PKG=https://releases.cortezaproject.org/files/corteza-webapp-${VERSION}.tar.gz
|
||||
|
||||
# Download package
|
||||
ADD $CORTEZA_CLIENT_WEB_AIO_PKG /tmp
|
||||
|
||||
RUN apk update && apk add --no-cache file
|
||||
|
||||
RUN rm -rf /html; mkdir /html
|
||||
WORKDIR /html
|
||||
|
||||
|
||||
RUN file "/tmp/$(basename $CORTEZA_CLIENT_WEB_AIO_PKG)" | grep -q 'gzip' && \
|
||||
tar zxvf "/tmp/$(basename $CORTEZA_CLIENT_WEB_AIO_PKG)" -C /html
|
||||
|
||||
# deploy-stage
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
COPY --from=build-stage /html /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
#COPY CONTRIBUTING.* DCO LICENSE README.* ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --start-period=10s --timeout=30s \
|
||||
CMD wget --quiet --tries=1 --spider "http://127.0.0.1:80/config.js" || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
3
client/web/admin/.browserslistrc
Normal file
3
client/web/admin/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
16
client/web/admin/.editorconfig
Normal file
16
client/web/admin/.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/admin/.eslintrc.js
Normal file
10
client/web/admin/.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/admin/.gitignore
vendored
Normal file
20
client/web/admin/.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/admin/Makefile
Normal file
39
client/web/admin/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-admin
|
||||
|
||||
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:
|
||||
$(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/admin/babel.config.js
Normal file
5
client/web/admin/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@vue/app', { useBuiltIns: 'entry' }],
|
||||
],
|
||||
}
|
||||
36
client/web/admin/jest.config.js
Normal file
36
client/web/admin/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-admin/(.*)$': '<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',
|
||||
],
|
||||
}
|
||||
94
client/web/admin/package.json
Normal file
94
client/web/admin/package.json
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"name": "corteza-webapp-admin",
|
||||
"description": "Corteza Admin WebApp",
|
||||
"version": "2024.9.10",
|
||||
"license": "Apache-2.0",
|
||||
"contributors": [
|
||||
"Denis Arh <denis.arh@gmail.com>"
|
||||
],
|
||||
"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.2",
|
||||
"axios": "^1.8.3",
|
||||
"bootstrap-vue": "2.23.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"echarts": "5.6.0",
|
||||
"fstream": "^1.0.12",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"minimalist": "^1.0.0",
|
||||
"minimist": "^1.2.6",
|
||||
"moment": "2.30.1",
|
||||
"portal-vue": "^2.1.7",
|
||||
"vue": "2.7.16",
|
||||
"vue-echarts": "7.0.3",
|
||||
"vue-native-websocket": "^2.0.15",
|
||||
"vue-router": "3.1.5",
|
||||
"vuedraggable": "^2.23.0",
|
||||
"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/admin/postcss.config.js
Normal file
6
client/web/admin/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
rtlcss: {},
|
||||
},
|
||||
}
|
||||
1
client/web/admin/public/.gitignore
vendored
Normal file
1
client/web/admin/public/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
config.js
|
||||
17
client/web/admin/public/config.example.js
Normal file
17
client/web/admin/public/config.example.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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';
|
||||
|
||||
// Set to true to enable i18next-pseudo
|
||||
// Used to test translation string in a development environment
|
||||
// Even if set to true, it will be disabled in production
|
||||
window.i18nPseudoModeEnabled = false
|
||||
9
client/web/admin/public/icon.svg
Normal file
9
client/web/admin/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 |
35
client/web/admin/public/index.html
Normal file
35
client/web/admin/public/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!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" id="corteza-custom-css" type="text/css" href="<%= BASE_URL %>custom.css">
|
||||
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
|
||||
<title><%= FLAVOUR %></title>
|
||||
</head>
|
||||
<body id="admin">
|
||||
<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>
|
||||
227
client/web/admin/src/app.js
Normal file
227
client/web/admin/src/app.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import './config-check'
|
||||
import './console-splash'
|
||||
|
||||
import './components'
|
||||
import './filters'
|
||||
import './mixins'
|
||||
import './plugins'
|
||||
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
import { system } from '@cortezaproject/corteza-js'
|
||||
import { corredor, i18n, mixins, websocket } from '@cortezaproject/corteza-vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const notProduction = (process.env.NODE_ENV !== 'production')
|
||||
|
||||
export default (options = {}) => {
|
||||
options = {
|
||||
el: '#app',
|
||||
name: 'admin',
|
||||
template: '<router-view v-if="loaded && i18nLoaded && isRbacLoaded" />',
|
||||
|
||||
mixins: [
|
||||
mixins.corredor,
|
||||
],
|
||||
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
i18nLoaded: false,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isRbacLoaded: 'rbac/isLoaded',
|
||||
}),
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
// switch the page directionality on body based on language
|
||||
document.body.setAttribute('dir', this.textDirectionality(user.meta.preferredLanguage))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// ref to vue is needed inside compose helper
|
||||
// load and register bundle and list of client/server scripts
|
||||
const bundleLoaderOpt = {
|
||||
// Name of the bundle to load
|
||||
bundle: 'admin',
|
||||
|
||||
// Debug logging
|
||||
verbose: notProduction,
|
||||
|
||||
// Context for exec function (client scripts only!)
|
||||
//
|
||||
// Extended with additional helpers
|
||||
ctx: new corredor.WebappCtx({
|
||||
$invoker: user,
|
||||
authToken: this.$auth.accessToken,
|
||||
}),
|
||||
}
|
||||
|
||||
// Load all pending prompts:
|
||||
this.$store.dispatch('wfPrompts/update')
|
||||
|
||||
// Only use enabled apis
|
||||
const enabledApis = [this.$SystemAPI, this.$ComposeAPI, this.$AutomationAPI]
|
||||
if (this.$Settings.get('federation.enabled', false)) {
|
||||
enabledApis.push(this.$FederationAPI)
|
||||
}
|
||||
|
||||
// Load effective permissions
|
||||
this.$store.dispatch('rbac/load', enabledApis)
|
||||
|
||||
// Initialize notifications
|
||||
this.$store.dispatch('notifications/fetchNotifications')
|
||||
|
||||
return this.loadBundle(bundleLoaderOpt)
|
||||
.then(() => this.$SystemAPI.automationList({ excludeInvalid: true }))
|
||||
.then(this.makeAutomationScriptsRegistrator(
|
||||
// compose specific handler that routes onManual events for server-scripts
|
||||
// to the proper endpoint on the API
|
||||
system.TriggerSystemServerScriptOnManual(this.$SystemAPI),
|
||||
))
|
||||
.then(() => {
|
||||
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 workflow messages
|
||||
this.$on('websocket-message', ({ data }) => {
|
||||
const msg = JSON.parse(data)
|
||||
switch (msg['@type']) {
|
||||
case 'workflowSessionPrompt':
|
||||
this.$store.dispatch('wfPrompts/new', msg['@value'])
|
||||
break
|
||||
|
||||
case 'workflowSessionResumed':
|
||||
this.$store.dispatch('wfPrompts/clear', msg['@value'])
|
||||
break
|
||||
|
||||
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-admin' },
|
||||
'admin',
|
||||
'automation.scripts',
|
||||
'automation.sessions',
|
||||
'automation.workflows',
|
||||
'compose.automation',
|
||||
'compose.settings',
|
||||
'dashboard',
|
||||
'federation.nodes',
|
||||
'general',
|
||||
'navigation',
|
||||
'notification',
|
||||
'notifications',
|
||||
'permissions',
|
||||
'system.actionlog',
|
||||
'system.apigw',
|
||||
'system.applications',
|
||||
'system.authclients',
|
||||
'system.code-snippets',
|
||||
'system.connections',
|
||||
'system.email',
|
||||
'system.queues',
|
||||
'system.roles',
|
||||
'system.sensitivityLevel',
|
||||
'system.settings',
|
||||
'system.templates',
|
||||
'system.user-groups',
|
||||
'system.users',
|
||||
'ui.settings',
|
||||
),
|
||||
|
||||
// Any additional options we want to merge
|
||||
...options,
|
||||
}
|
||||
|
||||
const app = new Vue(options)
|
||||
|
||||
// Simple HMR acceptance
|
||||
if (module.hot) {
|
||||
module.hot.accept()
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
113
client/web/admin/src/components/Apigw/CFilterModal.vue
Normal file
113
client/web/admin/src/components/Apigw/CFilterModal.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-modal
|
||||
:visible="visible"
|
||||
size="lg"
|
||||
:title="(filter || {}).label"
|
||||
:ok-title="$t('filters.modal.ok')"
|
||||
body-class="p-0"
|
||||
cancel-variant="light"
|
||||
@ok="onSave"
|
||||
@hidden="onHidden"
|
||||
>
|
||||
<div
|
||||
v-if="internalFilter"
|
||||
class="card-body"
|
||||
>
|
||||
<c-filter-params
|
||||
:filter="internalFilter"
|
||||
/>
|
||||
<b-form-checkbox
|
||||
v-model="internalFilter.enabled"
|
||||
data-test-id="checkbox-filter-enable"
|
||||
>
|
||||
{{ $t('filters.enabled') }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CFilterParams from 'corteza-webapp-admin/src/components/Apigw/CFilterParams'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CFilterParams,
|
||||
},
|
||||
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
internalFilter: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
visible: {
|
||||
handler (visible) {
|
||||
if (visible) {
|
||||
// Convert to FE structure
|
||||
this.internalFilter = {
|
||||
...this.filter,
|
||||
params: this.filter.params.map(p => {
|
||||
let value = p.value || {}
|
||||
|
||||
if (this.filter.ref === 'response') {
|
||||
if (p.type === 'header') {
|
||||
value = Object.entries(value).map(([name, v = []]) => ({ name, expr: v.join('') }))
|
||||
} else if (p.type === 'input') {
|
||||
value = { type: 'Any', expr: '', ...value }
|
||||
}
|
||||
}
|
||||
|
||||
return { ...p, value }
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
this.internalFilter = undefined
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSave () {
|
||||
// Convert to BE structure
|
||||
const filter = {
|
||||
...this.internalFilter,
|
||||
params: this.internalFilter.params.map(p => {
|
||||
if (this.filter.ref === 'response') {
|
||||
if (p.type === 'header') {
|
||||
p.value = p.value.reduce((obj, { name, expr = '' }) => {
|
||||
return { ...obj, [name]: [expr] }
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}),
|
||||
}
|
||||
|
||||
this.$emit('submit', { ...filter, updated: true })
|
||||
},
|
||||
|
||||
onHidden () {
|
||||
this.internalFilter = undefined
|
||||
this.$emit('reset')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
202
client/web/admin/src/components/Apigw/CFilterParams.vue
Normal file
202
client/web/admin/src/components/Apigw/CFilterParams.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div v-if="filter.params.length">
|
||||
<b-form-group
|
||||
v-for="(param, index) in filter.params"
|
||||
:key="index"
|
||||
class="text-primary"
|
||||
>
|
||||
<template slot="label">
|
||||
{{ $t(`filters.labels.${param.label}`) }}
|
||||
|
||||
<template v-if="param.label === 'expr'">
|
||||
<a
|
||||
v-if="param.label === 'expr'"
|
||||
:href="documentationURL"
|
||||
target="_blank"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'question-circle']"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- TODO create multi field component-->
|
||||
<b-form-checkbox
|
||||
v-if="param.type === 'bool'"
|
||||
v-model="param.value"
|
||||
/>
|
||||
|
||||
<c-input-select
|
||||
v-else-if="param.label === 'workflow'"
|
||||
v-model="param.value"
|
||||
:options="workflows"
|
||||
:get-option-key="getOptionKey"
|
||||
:reduce="wf => wf.workflowID"
|
||||
:placeholder="$t('filters.placeholders.workflow')"
|
||||
:loading="loadingWorkflows"
|
||||
/>
|
||||
|
||||
<b-form-select
|
||||
v-else-if="param.label === 'status'"
|
||||
v-model="param.value"
|
||||
:options="httpStatusOptions"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
:value="undefined"
|
||||
>
|
||||
{{ $t('filters.httpStatus.none') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
|
||||
<template v-else-if="filter.ref === 'response'">
|
||||
<template v-if="param.type === 'input'">
|
||||
<b-form-select
|
||||
v-model="param.value.type"
|
||||
:options="inputTypeOptions"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<b-input-group>
|
||||
<b-input-group-prepend>
|
||||
<b-input-group-text variant="extra-light">
|
||||
ƒ
|
||||
</b-input-group-text>
|
||||
</b-input-group-prepend>
|
||||
<b-form-input
|
||||
v-model="param.value.expr"
|
||||
:placeholder="$t('filters.help.expression.example')"
|
||||
/>
|
||||
</b-input-group>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<c-form-table-wrapper
|
||||
:labels="{ addButton: $t('filters.addHeader') }"
|
||||
@add-item="param.value.push({ name: '', expr: '' })"
|
||||
>
|
||||
<b-input-group
|
||||
v-for="(header, hIndex) in param.value"
|
||||
:key="`header-${hIndex}`"
|
||||
class="mb-2"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="header.name"
|
||||
:placeholder="$t('filters.labels.name')"
|
||||
/>
|
||||
<b-form-input
|
||||
v-model="header.expr"
|
||||
:placeholder="$t('filters.labels.value')"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
show-icon
|
||||
@confirmed="param.value.splice(hIndex, 1)"
|
||||
/>
|
||||
</b-input-group>
|
||||
</c-form-table-wrapper>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<b-form-textarea
|
||||
v-if="param.label === 'jsfunc'"
|
||||
v-model="param.value"
|
||||
max-rows="6"
|
||||
/>
|
||||
|
||||
<b-input-group v-else>
|
||||
<b-input-group-prepend
|
||||
v-if="param.label === 'expr'"
|
||||
>
|
||||
<b-input-group-text variant="extra-light">
|
||||
ƒ
|
||||
</b-input-group-text>
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-form-input
|
||||
v-if="param.label === 'expr'"
|
||||
v-model="param.value"
|
||||
:placeholder="$t('filters.help.expression.example')"
|
||||
/>
|
||||
<b-form-input
|
||||
v-else
|
||||
v-model="param.value"
|
||||
/>
|
||||
</b-input-group>
|
||||
</template>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
loadingWorkflows: false,
|
||||
workflows: [],
|
||||
|
||||
httpStatusOptions: [
|
||||
{ value: 300, text: this.$t('filters.httpStatus.300') },
|
||||
{ value: 301, text: this.$t('filters.httpStatus.301') },
|
||||
{ value: 302, text: this.$t('filters.httpStatus.302') },
|
||||
{ value: 303, text: this.$t('filters.httpStatus.303') },
|
||||
{ value: 304, text: this.$t('filters.httpStatus.304') },
|
||||
{ value: 307, text: this.$t('filters.httpStatus.307') },
|
||||
{ value: 308, text: this.$t('filters.httpStatus.308') },
|
||||
],
|
||||
|
||||
inputTypeOptions: [
|
||||
'String',
|
||||
'Any',
|
||||
'Array',
|
||||
'KV',
|
||||
'DateTime',
|
||||
'Float',
|
||||
'Integer',
|
||||
'Reader',
|
||||
'Vars',
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
documentationURL () {
|
||||
// eslint-disable-next-line no-undef
|
||||
const [year, month] = VERSION.split('.')
|
||||
return `https://docs.cortezaproject.org/corteza-docs/${year}.${month}/integrator-guide/expr/index.html`
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
if (this.filter.params.some(({ label = '' }) => label === 'workflow')) {
|
||||
this.loadingWorkflows = true
|
||||
|
||||
this.$AutomationAPI.workflowList()
|
||||
.then(({ set: workflows = [] }) => {
|
||||
this.workflows = workflows.map(({ workflowID, handle, meta }) => {
|
||||
return { label: meta.name || handle, workflowID }
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingWorkflows = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getOptionKey ({ workflowID }) {
|
||||
return workflowID
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
71
client/web/admin/src/components/Apigw/CFiltersDropdown.vue
Normal file
71
client/web/admin/src/components/Apigw/CFiltersDropdown.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<b-dropdown
|
||||
data-test-id="dropdown-add-filter"
|
||||
:text="$t('filters.addFilter')"
|
||||
variant="primary"
|
||||
>
|
||||
<template v-if="filterList.length">
|
||||
<b-dropdown-item-button
|
||||
v-for="(filter, index) in filterList"
|
||||
:key="index"
|
||||
:data-test-id="filterDropdownCypressId(filter.label)"
|
||||
:disabled="filter.disabled"
|
||||
@click="onAddFilter(filter)"
|
||||
>
|
||||
{{ filter.label }}
|
||||
</b-dropdown-item-button>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item-button
|
||||
v-else
|
||||
disabled
|
||||
>
|
||||
<span
|
||||
data-test-id="filter-list-empty"
|
||||
class="text-danger"
|
||||
>
|
||||
{{ $t('filters.filterListEmpty') }}
|
||||
</span>
|
||||
</b-dropdown-item-button>
|
||||
</b-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
availableFilters: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
filters: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
filterList () {
|
||||
return this.availableFilters.map(f => {
|
||||
return { ...f, disabled: !!(this.filters || []).some(filter => filter.ref === f.ref) }
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAddFilter (filter) {
|
||||
const add = { ...filter, created: true, params: [] }
|
||||
const { params = [] } = filter
|
||||
|
||||
for (const p of params) {
|
||||
add.params.push({ ...p, options: { ...p.options } })
|
||||
}
|
||||
|
||||
this.$emit('addFilter', add)
|
||||
},
|
||||
|
||||
filterDropdownCypressId (filter) {
|
||||
return filter.toLowerCase().split(' ').join('-')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
206
client/web/admin/src/components/Apigw/CFiltersStepper.vue
Normal file
206
client/web/admin/src/components/Apigw/CFiltersStepper.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-filter-list"
|
||||
body-class="p-0"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm mt-3"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('filters.title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-tabs
|
||||
data-test-id="filter-steps"
|
||||
card
|
||||
>
|
||||
<b-tab
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:button-id="steps[index]"
|
||||
:title="$t(`filters.step_title.${step}`)"
|
||||
class="border-0 p-0"
|
||||
@click="onActivateTab(index)"
|
||||
>
|
||||
<c-filters-dropdown
|
||||
:available-filters="getAvailableFiltersByStep"
|
||||
:filters="getSelectedFiltersByStep"
|
||||
class="p-3"
|
||||
@addFilter="onAddFilter"
|
||||
/>
|
||||
|
||||
<c-filters-table
|
||||
:filters="getSelectedFiltersByStep"
|
||||
:step="index"
|
||||
:fetching="fetching"
|
||||
@filterSelect="onFilterSelect"
|
||||
@removeFilter="onRemoveFilter"
|
||||
@sortFilters="onSortFilters"
|
||||
/>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:disabled="disabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<c-filter-modal
|
||||
:visible="!!selectedFilter"
|
||||
:filter="selectedFilter"
|
||||
@submit="onSubmit"
|
||||
@reset="onReset"
|
||||
/>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CFilterModal from 'corteza-webapp-admin/src/components/Apigw/CFilterModal'
|
||||
import CFiltersTable from 'corteza-webapp-admin/src/components/Apigw/CFiltersTable'
|
||||
import CFiltersDropdown from 'corteza-webapp-admin/src/components/Apigw/CFiltersDropdown'
|
||||
|
||||
const mapKindToStep = {
|
||||
prefilter: 0,
|
||||
processer: 1,
|
||||
postfilter: 2,
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CFilterModal,
|
||||
CFiltersTable,
|
||||
CFiltersDropdown,
|
||||
},
|
||||
|
||||
props: {
|
||||
fetching: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
filters: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
availableFilters: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
steps: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
selectedFilter: null,
|
||||
selectedTab: 0,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
disabled () {
|
||||
return !(this.filters.some(({ updated, created, deleted }) => updated || created || deleted))
|
||||
},
|
||||
|
||||
getSelectedFilter () {
|
||||
return this.selectedFilter ? this.selectedFilter : null
|
||||
},
|
||||
|
||||
getAvailableFiltersByStep () {
|
||||
return (this.availableFilters || []).filter(({ kind }) => {
|
||||
return mapKindToStep[kind] === this.selectedTab
|
||||
})
|
||||
},
|
||||
|
||||
getSelectedFiltersByStep () {
|
||||
return (this.filters || []).filter(({ kind, deleted }) => {
|
||||
return mapKindToStep[kind] === this.selectedTab && !deleted
|
||||
}).sort((a, b) => a.weight - b.weight)
|
||||
},
|
||||
|
||||
disabledRemoveButton () {
|
||||
return !this.filters.some(({ options }) => (options || { checked: false }).checked)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAddFilter (filter) {
|
||||
const i = this.filters.findIndex(({ ref, deleted }) => ref === filter.ref && !deleted)
|
||||
|
||||
if (i < 0) {
|
||||
this.selectedFilter = filter
|
||||
} else {
|
||||
this.selectedFilter = this.filters[i]
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit (filter) {
|
||||
const i = this.filters.findIndex(({ ref, deleted }) => ref === filter.ref && !deleted)
|
||||
|
||||
if (i < 0) {
|
||||
filter.weight = this.getSelectedFiltersByStep.length
|
||||
this.filters.push(filter)
|
||||
} else {
|
||||
this.filters.splice(this.filters.findIndex(({ ref, deleted }) => ref === filter.ref && !deleted), 1, filter)
|
||||
}
|
||||
|
||||
this.$emit('update:filters', this.filters)
|
||||
},
|
||||
|
||||
onReset () {
|
||||
this.selectedFilter = undefined
|
||||
},
|
||||
|
||||
onSortFilters (sortedFilters) {
|
||||
this.filters.forEach(filter => {
|
||||
const i = sortedFilters.findIndex(({ ref, deleted }) => ref === filter.ref && !deleted)
|
||||
|
||||
if (i >= 0) {
|
||||
filter.weight = sortedFilters.findIndex(({ ref, deleted }) => ref === filter.ref && !deleted)
|
||||
filter.updated = true
|
||||
}
|
||||
})
|
||||
this.filters.sort((a, b) => a.weight - b.weight)
|
||||
},
|
||||
|
||||
onRemoveFilter (filter) {
|
||||
if (filter.filterID) {
|
||||
this.filters.splice(this.filters.findIndex(({ filterID, deleted }) => filterID === filter.filterID && !deleted), 1, { ...filter, deleted: true })
|
||||
} else {
|
||||
this.filters.splice(this.filters.findIndex(({ ref, deleted }) => ref === filter.ref && !deleted), 1)
|
||||
}
|
||||
|
||||
this.$emit('update:filters', this.filters)
|
||||
},
|
||||
|
||||
onFilterSelect (filter = {}) {
|
||||
this.selectedFilter = { ...filter }
|
||||
},
|
||||
|
||||
onActivateTab (index) {
|
||||
this.selectedTab = index
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
139
client/web/admin/src/components/Apigw/CFiltersTable.vue
Normal file
139
client/web/admin/src/components/Apigw/CFiltersTable.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table-simple
|
||||
hover
|
||||
class="mb-0"
|
||||
>
|
||||
<b-thead head-variant="light">
|
||||
<b-tr>
|
||||
<b-th />
|
||||
<b-th>{{ $t('filters.list.filters') }}</b-th>
|
||||
<b-th>{{ $t('filters.list.status') }}</b-th>
|
||||
<b-th />
|
||||
</b-tr>
|
||||
</b-thead>
|
||||
|
||||
<draggable
|
||||
v-if="!fetching"
|
||||
v-model="sortableFilters"
|
||||
:options="{ handle: '.handle' }"
|
||||
tag="b-tbody"
|
||||
>
|
||||
<b-tr
|
||||
v-for="(filter, index) in sortableFilters"
|
||||
:key="index"
|
||||
class="pointer"
|
||||
@click="onRowClick(filter, index)"
|
||||
>
|
||||
<td
|
||||
class="handle align-middle grab"
|
||||
style="width: 1%"
|
||||
@click.stop
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'bars']"
|
||||
class="text-secondary"
|
||||
/>
|
||||
</td>
|
||||
<b-td class="align-middle">
|
||||
{{ filter.label }}
|
||||
</b-td>
|
||||
<b-td class="align-middle">
|
||||
{{ $t(`filters.${filter.enabled ? 'enabled' : 'disabled'}`) }}
|
||||
</b-td>
|
||||
<b-td class="text-right align-middle">
|
||||
<c-input-confirm
|
||||
show-icon
|
||||
class="ml-1"
|
||||
@confirmed="onRemoveFilter(filter)"
|
||||
@click.stop
|
||||
/>
|
||||
</b-td>
|
||||
</b-tr>
|
||||
</draggable>
|
||||
</b-table-simple>
|
||||
|
||||
<div
|
||||
class="d-flex flex-column align-items-center justify-content-center h-100 overflow-hidden"
|
||||
>
|
||||
<b-spinner
|
||||
v-if="fetching"
|
||||
class="my-4"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-else-if="!sortableFilters.length"
|
||||
data-test-id="no-filters"
|
||||
class="my-4"
|
||||
>
|
||||
{{ $t('filters.list.noFilters') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
},
|
||||
|
||||
props: {
|
||||
filters: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: () => 0,
|
||||
},
|
||||
fetching: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
selectedRow: 0,
|
||||
selectedFilter: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
sortableFilters: {
|
||||
get () {
|
||||
return this.filters
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.$emit('sortFilters', v)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAddFilter (filter) {
|
||||
if (!this.filters.find(f => f.ref === filter.ref)) {
|
||||
this.filters.push({ ...filter })
|
||||
}
|
||||
|
||||
if (this.filters.length === 1) {
|
||||
this.$emit('filterSelect', filter)
|
||||
}
|
||||
},
|
||||
|
||||
onRemoveFilter (filter) {
|
||||
this.$emit('removeFilter', filter)
|
||||
},
|
||||
|
||||
onRowClick (filter, index) {
|
||||
this.selectedRow = index
|
||||
this.selectedFilter = filter
|
||||
this.$emit('filterSelect', filter)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
227
client/web/admin/src/components/Apigw/CRouteEditorInfo.vue
Normal file
227
client/web/admin/src/components/Apigw/CRouteEditorInfo.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-route-edit"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', route)"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:description="routeEndpointDescription"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<template
|
||||
#label
|
||||
>
|
||||
<label
|
||||
label-for="endpoint"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('endpoint') }}
|
||||
</label>
|
||||
|
||||
<font-awesome-icon
|
||||
id="endpoint_info"
|
||||
class="ml-1"
|
||||
:icon="['far', 'question-circle']"
|
||||
/>
|
||||
|
||||
<b-tooltip
|
||||
target="endpoint_info"
|
||||
triggers="hover"
|
||||
>
|
||||
{{ $t('tooltip') }}
|
||||
</b-tooltip>
|
||||
</template>
|
||||
|
||||
<b-form-input
|
||||
id="endpoint"
|
||||
v-model="route.endpoint"
|
||||
data-test-id="input-endpoint"
|
||||
:state="isValidEndpoint"
|
||||
required
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('method')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="route.method"
|
||||
data-test-id="select-method"
|
||||
:options="methods"
|
||||
required
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="route.meta"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="route.meta.description"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('enabled')"
|
||||
:class="{ 'mb-0': !route.routeID }"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="route.enabled"
|
||||
switch
|
||||
:labels="checkboxLabel"
|
||||
data-test-id="checkbox-enabled"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:id="route.routeID"
|
||||
:resource="route"
|
||||
/>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="route && route.routeID && route.canDeleteApigwRoute"
|
||||
:data-test-id="deletedButtonStatusCypressId"
|
||||
variant="danger"
|
||||
:text="getDeleteStatus"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', route)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
name: 'CRouteEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: ['system.apigw'],
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
props: {
|
||||
route: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||
checkboxLabel: {
|
||||
on: this.$t('general:label.general.yes'),
|
||||
off: this.$t('general:label.general.no'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return !this.route.routeID || this.route.routeID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : true // this.route.canUpdateRoute
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.isValidEndpoint].includes(false)
|
||||
},
|
||||
|
||||
getDeleteStatus () {
|
||||
return this.route.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
|
||||
isValidEndpoint () {
|
||||
const { endpoint } = this.route
|
||||
|
||||
return (!!endpoint && /^(\/[\w-]+)+$/.test(endpoint)) ? null : false
|
||||
},
|
||||
|
||||
startsWithSlash () {
|
||||
return this.route.endpoint ? /^\//.test(this.route.endpoint) : null
|
||||
},
|
||||
|
||||
routeEndpointDescription () {
|
||||
if (this.isValidEndpoint === false) {
|
||||
if (!this.startsWithSlash) {
|
||||
return this.$t('validation.slash')
|
||||
} else if (this.route.endpoint.length < 2) {
|
||||
return this.$t('validation.minLength')
|
||||
} else if (!/^([\w-]+)+$/.test(this.route.endpoint)) {
|
||||
return this.$t('validation.invalidCharacters')
|
||||
}
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
deletedButtonStatusCypressId () {
|
||||
return `button-${this.getDeleteStatus.toLowerCase()}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
236
client/web/admin/src/components/Apigw/CRouteList.vue
Normal file
236
client/web/admin/src/components/Apigw/CRouteList.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<b-card
|
||||
class="shadow-sm"
|
||||
header-class="border-bottom"
|
||||
body-class="p-0"
|
||||
>
|
||||
<template
|
||||
#header
|
||||
>
|
||||
<h4 class="mb-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<c-resource-list
|
||||
:primary-key="primaryKey"
|
||||
:filter="filter"
|
||||
:sorting="sorting"
|
||||
:pagination="pagination"
|
||||
:fields="fields"
|
||||
:items="items"
|
||||
:row-class="genericRowClass"
|
||||
:translations="{
|
||||
searchPlaceholder: $t('filterForm.query.placeholder'),
|
||||
notFound: $t('admin:general.notFound'),
|
||||
noItems: $t('admin:general.resource-list.no-items'),
|
||||
loading: $t('loading'),
|
||||
showingPagination: 'admin:general.pagination.showing',
|
||||
singlePluralPagination: 'admin:general.pagination.single',
|
||||
prevPagination: $t('admin:general.pagination.prev'),
|
||||
nextPagination: $t('admin:general.pagination.next'),
|
||||
resourceSingle: $t('general:label.route.single'),
|
||||
resourcePlural: $t('general:label.route.plural')
|
||||
|
||||
}"
|
||||
clickable
|
||||
card-header-class="rounded-0"
|
||||
class="h-100 bg-transparent"
|
||||
@search="filterList"
|
||||
@row-clicked="handleRowClicked"
|
||||
>
|
||||
<template #header>
|
||||
<b-button
|
||||
v-if="canCreate"
|
||||
data-test-id="button-add"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
:to="{ name: 'system.apigw.new' }"
|
||||
>
|
||||
{{ $t('new') }}
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-if="$Settings.get('apigw.profiler.enabled', false)"
|
||||
data-test-id="button-profiler"
|
||||
variant="info"
|
||||
size="lg"
|
||||
:to="{ name: 'system.apigw.profiler' }"
|
||||
>
|
||||
{{ $t('profiler') }}
|
||||
</b-button>
|
||||
|
||||
<c-permissions-button
|
||||
v-if="canGrant"
|
||||
data-test-id="button-permissions"
|
||||
resource="corteza::system:apigw-route/*"
|
||||
:button-label="$t('permissions')"
|
||||
size="lg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #toolbar>
|
||||
<c-resource-list-status-filter
|
||||
v-model="filter.deleted"
|
||||
data-test-id="filter-deleted-routes"
|
||||
:label="$t('filterForm.deleted.label')"
|
||||
:excluded-label="$t('filterForm.excluded.label')"
|
||||
:inclusive-label="$t('filterForm.inclusive.label')"
|
||||
:exclusive-label="$t('filterForm.exclusive.label')"
|
||||
@change="filterList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions="{ item: r }">
|
||||
<b-dropdown
|
||||
v-if="(areActionsVisible({ resource: r, conditions: ['canDeleteApigwRoute', 'canGrant'] }))"
|
||||
boundary="viewport"
|
||||
variant="outline-extra-light"
|
||||
toggle-class="d-flex align-items-center justify-content-center text-primary border-0 py-2"
|
||||
no-caret
|
||||
dropleft
|
||||
lazy
|
||||
menu-class="m-0"
|
||||
>
|
||||
<template #button-content>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'ellipsis-v']"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<c-permissions-button
|
||||
v-if="r.routeID && canGrant"
|
||||
:title="r.endpoint || r.routeID"
|
||||
:target="r.endpoint || r.routeID"
|
||||
:resource="`corteza::system:apigw-route/${r.routeID}`"
|
||||
:button-label="$t('permissions')"
|
||||
class="dropdown-item"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
v-if="r.canDeleteApigwRoute"
|
||||
:text="getActionText(r)"
|
||||
show-icon
|
||||
:icon="getActionIcon(r)"
|
||||
borderless
|
||||
variant="link"
|
||||
size="md"
|
||||
button-class="dropdown-item"
|
||||
icon-class="text-danger"
|
||||
class="w-100"
|
||||
@confirmed="handleDelete(r)"
|
||||
/>
|
||||
</b-dropdown>
|
||||
</template>
|
||||
</c-resource-list>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import listHelpers from 'corteza-webapp-admin/src/mixins/listHelpers'
|
||||
import moment from 'moment'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CResourceList } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CResourceList,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
listHelpers,
|
||||
],
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.apigw',
|
||||
keyPrefix: 'list',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
primaryKey: 'routeID',
|
||||
editRoute: 'system.apigw.edit',
|
||||
|
||||
filter: {
|
||||
query: '',
|
||||
deleted: 0,
|
||||
},
|
||||
|
||||
sorting: {
|
||||
sortBy: 'createdAt',
|
||||
sortDesc: true,
|
||||
},
|
||||
|
||||
fields: [
|
||||
{
|
||||
key: 'endpoint',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'method',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
formatter: (v) => v ? 'Yes' : 'No',
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
sortable: true,
|
||||
formatter: (v) => moment(v).fromNow(),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
class: 'actions',
|
||||
},
|
||||
].map(c => ({
|
||||
...c,
|
||||
// Generate column label translation key
|
||||
label: this.$t(`columns.${c.key}`),
|
||||
})),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
can: 'rbac/can',
|
||||
}),
|
||||
|
||||
canCreate () {
|
||||
return this.can('system/', 'apigw-route.create')
|
||||
},
|
||||
|
||||
canGrant () {
|
||||
return this.can('system/', 'grant')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
incLoader: 'ui/incLoader',
|
||||
decLoader: 'ui/decLoader',
|
||||
}),
|
||||
|
||||
items () {
|
||||
return this.procListResults(this.$SystemAPI.apigwRouteListCancellable(this.encodeListParams()))
|
||||
},
|
||||
|
||||
handleDelete (route) {
|
||||
this.handleItemDelete({
|
||||
resource: route,
|
||||
resourceName: 'apigwRoute',
|
||||
locale: 'gateway',
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.route-list {
|
||||
.card-header {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
117
client/web/admin/src/components/Apigw/CSettingsEditor.vue
Normal file
117
client/web/admin/src/components/Apigw/CSettingsEditor.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form-row
|
||||
@submit.prevent="$emit('submit', settings)"
|
||||
>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('profiler.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-radio-group
|
||||
v-model="profilerSetting"
|
||||
:options="profilerOptions"
|
||||
button-variant="outline-primary"
|
||||
size="sm"
|
||||
buttons
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('proxy.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="settings['apigw.proxy.follow-redirects']"
|
||||
>
|
||||
{{ $t('proxy.follow') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-form-row>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', settings)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'CSettingsEditor',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.apigw',
|
||||
keyPrefix: 'settings',
|
||||
},
|
||||
|
||||
props: {
|
||||
settings: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
profilerOptions: [
|
||||
{ value: 'disabled', text: 'Disabled' },
|
||||
{ value: 'filter', text: 'Enabled as filter' },
|
||||
{ value: 'global', text: 'Enabled for all routes' },
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
profilerSetting: {
|
||||
get () {
|
||||
if (this.settings['apigw.profiler.enabled']) {
|
||||
return this.settings['apigw.profiler.global'] ? 'global' : 'filter'
|
||||
}
|
||||
|
||||
return 'disabled'
|
||||
},
|
||||
|
||||
set (setting) {
|
||||
this.settings['apigw.profiler.enabled'] = ['filter', 'global'].includes(setting)
|
||||
this.settings['apigw.profiler.global'] = setting === 'global'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-card
|
||||
data-test-id="card-general-info"
|
||||
header-class="border-bottom"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('general.label') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.id')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="hit.ID"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.route')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="request.route"
|
||||
data-test-id="input-route"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.URL')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="request.url"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.method')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="request.method"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.statusCode')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-center h-100"
|
||||
>
|
||||
<h5 class="mb-0">
|
||||
<b-badge :variant="getStatusCodeVariant(request.statusCode)">
|
||||
{{ request.statusCode }}
|
||||
</b-badge>
|
||||
</h5>
|
||||
</div>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.remoteAddress')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="request.remoteAddress"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.duration')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="request.duration"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.start')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="request.start"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general.end')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="request.end"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-button
|
||||
v-if="showOpenRoute"
|
||||
variant="light"
|
||||
:to="{ name: 'system.apigw.edit', params: { routeID: request.routeID } }"
|
||||
>
|
||||
{{ $t('general.openRoute') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
class="shadow-sm mt-3"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('headers.label') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
v-for="header in request.headers"
|
||||
:key="header.label"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="header.label"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="header.value"
|
||||
plaintext
|
||||
disabled
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
body-class="p-0"
|
||||
class="shadow-sm mt-3 overflow-hidden"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('body.label') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<c-ace-editor
|
||||
:value="request.body"
|
||||
:border="false"
|
||||
lang="json"
|
||||
min-height="400px"
|
||||
show-line-numbers
|
||||
read-only
|
||||
/>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import { fmt, NoID } from '@cortezaproject/corteza-js'
|
||||
|
||||
const { CAceEditor } = components
|
||||
|
||||
export default {
|
||||
name: 'CProfilerHitInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: ['system.apigw'],
|
||||
keyPrefix: 'profiler.hit',
|
||||
},
|
||||
|
||||
components: {
|
||||
CAceEditor,
|
||||
},
|
||||
|
||||
props: {
|
||||
hit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
request () {
|
||||
// eslint-disable-next-line camelcase
|
||||
const { request = {}, body = '', route = NoID, time_duration: duration = 0, time_start, time_finish, http_status_code: statusCode } = this.hit || {}
|
||||
const { URL = {}, RequestURI, Method, RemoteAddr, Header = {} } = request
|
||||
const { Path } = URL
|
||||
const headers = Object.entries(Header).map(([key, value = []]) => {
|
||||
return { label: key, value: value.join('') }
|
||||
})
|
||||
|
||||
// Try to parse body as json
|
||||
let jsonBody = atob(body)
|
||||
try {
|
||||
jsonBody = JSON.stringify(JSON.parse(jsonBody), null, 2)
|
||||
} catch (e) {}
|
||||
|
||||
return {
|
||||
routeID: route,
|
||||
route: Path,
|
||||
url: RequestURI,
|
||||
method: Method,
|
||||
statusCode,
|
||||
remoteAddress: RemoteAddr,
|
||||
duration: `${duration.toFixed(2)} ms`,
|
||||
start: fmt.fullDateTime(time_start),
|
||||
end: fmt.fullDateTime(time_finish),
|
||||
headers,
|
||||
body: jsonBody,
|
||||
}
|
||||
},
|
||||
|
||||
showOpenRoute () {
|
||||
return this.request.routeID !== NoID
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getStatusCodeVariant (statusCode = '') {
|
||||
const codeVariants = {
|
||||
2: 'success',
|
||||
3: 'info',
|
||||
4: 'danger',
|
||||
5: 'warning',
|
||||
}
|
||||
|
||||
return codeVariants[statusCode[0]]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-requests"
|
||||
no-body
|
||||
footer-class="border-top border-top border-top d-flex align-items-center justify-content-center"
|
||||
header-class="border-bottom"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="mb-0">
|
||||
{{ $t('general:label.requests') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<div class="d-flex align-items-center flex-wrap flex-fill p-3 gap-1">
|
||||
<div class="flex-fill">
|
||||
<b-button
|
||||
data-test-id="button-refresh"
|
||||
variant="primary"
|
||||
:disabled="loading"
|
||||
@click="loadItems()"
|
||||
>
|
||||
{{ $t('general:label.refresh') }}
|
||||
</b-button>
|
||||
<span
|
||||
class="ml-1"
|
||||
:class="{ 'loading': loading }"
|
||||
>
|
||||
{{ autoRefreshLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<c-input-confirm
|
||||
:disabled="!items.length"
|
||||
:processing="processingPurge"
|
||||
:text="$t('purge.this')"
|
||||
variant="danger"
|
||||
@confirmed="purgeRequests"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<b-table
|
||||
id="hit-list"
|
||||
hover
|
||||
responsive
|
||||
head-variant="light"
|
||||
class="mb-0"
|
||||
primary-key="hitID"
|
||||
:sort-by.sync="sorting.sortBy"
|
||||
:sort-desc.sync="sorting.sortDesc"
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
:busy="loading"
|
||||
no-local-sorting
|
||||
@sort-changed="resetItems"
|
||||
>
|
||||
<template #cell(http_method)="row">
|
||||
{{ row.item.request.Method }}
|
||||
</template>
|
||||
<template #cell(http_status_code)="row">
|
||||
<h6 class="mb-0">
|
||||
<b-badge :variant="getStatusCodeVariant(row.item.http_status_code)">
|
||||
{{ row.item.http_status_code }}
|
||||
</b-badge>
|
||||
</h6>
|
||||
</template>
|
||||
<template #cell(content_length)="row">
|
||||
{{ `${((row.item.request.ContentLength || 0) / 1000).toFixed(3)} kB` }}
|
||||
</template>
|
||||
<template #cell(actions)="row">
|
||||
<b-button
|
||||
data-test-id="button-edit-route"
|
||||
variant="link"
|
||||
class="p-0"
|
||||
:to="{ name: 'system.apigw.profiler.hit', params: { hitID: row.item.hitID } }"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'info-circle']"
|
||||
class="text-primary"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card-body>
|
||||
|
||||
<template #footer>
|
||||
<b-button
|
||||
v-if="items.length"
|
||||
variant="light"
|
||||
:disabled="!hasNextPage || loading"
|
||||
@click="loadMore()"
|
||||
>
|
||||
{{ $t('general:label.loadMore') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fmt } from '@cortezaproject/corteza-js'
|
||||
import listHelpers from 'corteza-webapp-admin/src/mixins/listHelpers'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
listHelpers,
|
||||
],
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: ['system.apigw'],
|
||||
keyPrefix: 'profiler',
|
||||
},
|
||||
|
||||
props: {
|
||||
// Must be base64
|
||||
route: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processingPurge: false,
|
||||
|
||||
filter: {
|
||||
routeID: '',
|
||||
next: '',
|
||||
before: '',
|
||||
query: '',
|
||||
},
|
||||
|
||||
sorting: {
|
||||
sortBy: 'time_start',
|
||||
sortDesc: true,
|
||||
},
|
||||
|
||||
totalItems: 0,
|
||||
|
||||
items: [],
|
||||
|
||||
refresh: {
|
||||
timer: undefined,
|
||||
countdown: 0,
|
||||
},
|
||||
|
||||
fields: [
|
||||
{
|
||||
key: 'time_start',
|
||||
sortable: true,
|
||||
formatter: v => v ? fmt.fullDateTime(v) : '',
|
||||
},
|
||||
{
|
||||
key: 'time_finish',
|
||||
sortable: true,
|
||||
formatter: v => v ? fmt.fullDateTime(v) : '',
|
||||
},
|
||||
{
|
||||
key: 'http_method',
|
||||
sortable: true,
|
||||
class: 'text-center',
|
||||
},
|
||||
{
|
||||
key: 'http_status_code',
|
||||
sortable: true,
|
||||
class: 'text-center',
|
||||
},
|
||||
{
|
||||
key: 'content_length',
|
||||
sortable: true,
|
||||
formatter: v => `${v || 0} bytes`,
|
||||
class: 'text-right',
|
||||
},
|
||||
{
|
||||
key: 'time_duration',
|
||||
sortable: true,
|
||||
formatter: v => v ? `${v.toFixed(2)} ms` : '',
|
||||
class: 'text-right',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
class: 'text-right',
|
||||
},
|
||||
].map(c => ({
|
||||
...c,
|
||||
// Generate column label translation key
|
||||
label: this.$t(`columns.${c.key}`),
|
||||
})),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
loading () {
|
||||
return !this.refresh.countdown
|
||||
},
|
||||
|
||||
autoRefreshLabel () {
|
||||
return !this.loading ? this.$t('refreshingIn', { seconds: this.refresh.countdown }) : this.$t('general:label.loading')
|
||||
},
|
||||
|
||||
hasNextPage () {
|
||||
return this.filter.next
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
route: {
|
||||
immediate: true,
|
||||
handler (route) {
|
||||
if (route) {
|
||||
this.resetItems()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.clearRefresh()
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadItems ({ append = false } = {}) {
|
||||
this.clearRefresh()
|
||||
|
||||
const oldBeforeID = this.filter.before
|
||||
this.filter.before = append ? this.filter.before : ''
|
||||
this.filter.routeID = this.route
|
||||
this.pagination.limit = append ? 10 : this.totalItems
|
||||
|
||||
const { response } = this.$SystemAPI.apigwProfilerRouteCancellable(this.encodeListParams())
|
||||
response().then(({ filter = {}, set = [] }) => {
|
||||
const { next } = filter
|
||||
this.filter = { ...this.filter, next }
|
||||
this.items = [
|
||||
...(append ? this.items : []),
|
||||
...set.map(i => ({ ...i, hitID: i.ID })),
|
||||
]
|
||||
this.totalItems = append ? this.totalItems + set.length : this.totalItems
|
||||
|
||||
return { filter, set }
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:gateway.profiler.fetch.error')))
|
||||
.finally(() => {
|
||||
if (!append) {
|
||||
this.filter.before = oldBeforeID
|
||||
}
|
||||
this.startRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
purgeRequests () {
|
||||
this.processingPurge = false
|
||||
|
||||
const { routeID } = this.filter
|
||||
this.$SystemAPI.apigwProfilerPurge({ routeID })
|
||||
.then(() => {
|
||||
this.loadItems()
|
||||
this.toastSuccess(this.$t('notification:gateway.profiler.purge.success'))
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:gateway.profiler.purge.error')))
|
||||
.finally(() => {
|
||||
this.processingPurge = true
|
||||
})
|
||||
},
|
||||
|
||||
resetItems (sorting = this.sorting) {
|
||||
this.sorting = sorting
|
||||
this.filter.before = ''
|
||||
this.totalItems = 10
|
||||
this.loadItems()
|
||||
},
|
||||
|
||||
loadMore () {
|
||||
this.filter.before = this.filter.next
|
||||
this.loadItems({ append: true })
|
||||
},
|
||||
|
||||
// To start the refresh countdown
|
||||
startRefresh () {
|
||||
this.refresh.countdown = 10
|
||||
this.resetRefresh()
|
||||
},
|
||||
|
||||
// If you need to temporarily stop the refresh countdown
|
||||
clearRefresh () {
|
||||
this.refresh.timer = clearTimeout(this.refresh.timer)
|
||||
this.refresh.countdown = 0
|
||||
},
|
||||
|
||||
resetRefresh () {
|
||||
clearTimeout(this.refresh.timer)
|
||||
this.refresh.timer = setTimeout(() => {
|
||||
this.refresh.countdown--
|
||||
if (this.refresh.countdown) {
|
||||
this.resetRefresh()
|
||||
} else {
|
||||
this.loadItems()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
getStatusCodeVariant (statusCode = '') {
|
||||
const codeVariants = {
|
||||
2: 'success',
|
||||
3: 'info',
|
||||
4: 'danger',
|
||||
5: 'warning',
|
||||
}
|
||||
|
||||
return codeVariants[statusCode[0]]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
2
client/web/admin/src/components/Application/C3.js
Normal file
2
client/web/admin/src/components/Application/C3.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as CApplicationEditorInfo } from './CApplicationEditorInfo.c3'
|
||||
export { default as CApplicationEditorUnify } from './CApplicationEditorUnify.c3'
|
||||
@@ -0,0 +1,46 @@
|
||||
import { default as component } from './CApplicationEditorInfo.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
application: { applicationID: '' },
|
||||
processing: false,
|
||||
success: false,
|
||||
canCreate: true,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Editor info',
|
||||
group: ['Applications'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('CanCreate', 'canCreate'),
|
||||
checkbox('Enable delete', {
|
||||
value (p) { return p.application.applicationID.length > 0 },
|
||||
update (p, val) { p.application.applicationID = val ? '123456789' : '' },
|
||||
}),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
application: {
|
||||
...props.application,
|
||||
enabled: false,
|
||||
name: '',
|
||||
},
|
||||
canCreate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-application-info"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', application)"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="application.name"
|
||||
data-test-id="input-name"
|
||||
:state="nameState"
|
||||
required
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('enabled')"
|
||||
:class="{ 'mb-0': !application.applicationID }"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="application.enabled"
|
||||
data-test-id="checkbox-enabled"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:id="application.applicationID"
|
||||
:resource="application"
|
||||
/>
|
||||
|
||||
<!--
|
||||
include hidden input to enable
|
||||
trigger submit event w/ ENTER
|
||||
-->
|
||||
<input
|
||||
type="submit"
|
||||
class="d-none"
|
||||
:disabled="saveDisabled"
|
||||
>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="application && application.applicationID && application.canDeleteApplication"
|
||||
:data-test-id="deleteButtonStatusCypressId"
|
||||
:text="getDeleteStatus"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', application)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
name: 'CApplicationEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.applications',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
props: {
|
||||
application: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
checkboxLabel: {
|
||||
on: this.$t('general:label.general.yes'),
|
||||
off: this.$t('general:label.general.no'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return !this.application.applicationID || this.application.applicationID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : this.application.canUpdateApplication
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.nameState].includes(false)
|
||||
},
|
||||
|
||||
nameState () {
|
||||
const { name } = this.application
|
||||
return name ? null : false
|
||||
},
|
||||
|
||||
getDeleteStatus () {
|
||||
return this.application.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
|
||||
deleteButtonStatusCypressId () {
|
||||
return `button-${this.getDeleteStatus.toLowerCase()}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
import { default as component } from './CApplicationEditorUnify.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
application: {},
|
||||
|
||||
unify: {
|
||||
config: '"data"',
|
||||
listed: true,
|
||||
logo: '/applications/low-code-platform.png',
|
||||
pinned: false,
|
||||
name: 'Namespaces',
|
||||
url: '/compose',
|
||||
},
|
||||
|
||||
canPin: true,
|
||||
processing: false,
|
||||
success: false,
|
||||
canCreate: true,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Editor unify',
|
||||
group: ['Applications'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
unify: {
|
||||
...props.unify,
|
||||
listed: false,
|
||||
name: '',
|
||||
config: '',
|
||||
},
|
||||
canPin: false,
|
||||
canCreate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-application-selector"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', unify)"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name.label')"
|
||||
:description="$t('name.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="unify.name"
|
||||
data-test-id="input-name"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('logo.label')"
|
||||
:description="$t('logo.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<template #label>
|
||||
<div
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
{{ $t('logo.label') }}
|
||||
|
||||
<b-button
|
||||
v-if="showLogoPreview"
|
||||
v-b-modal.logo
|
||||
data-test-id="button-logo-show"
|
||||
variant="link"
|
||||
class="d-flex align-items-center border-0 p-0 ml-2"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'eye']"
|
||||
/>
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-if="showLogoPreview"
|
||||
data-test-id="button-logo-reset"
|
||||
variant="light"
|
||||
size="sm"
|
||||
class="py-0 ml-2"
|
||||
@click="resetLogo()"
|
||||
>
|
||||
{{ $t('logo.reset') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<b-form-file
|
||||
v-model="unifyAssets.logo"
|
||||
data-test-id="file-logo-upload"
|
||||
accept="image/*"
|
||||
:placeholder="$t('logo.placeholder')"
|
||||
@change="$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('url.label')"
|
||||
:description="$t('url.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="unify.url"
|
||||
data-test-id="input-url"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('listed')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="unify.listed"
|
||||
data-test-id="checkbox-listed"
|
||||
switch
|
||||
:labels="checkboxLabel"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="canPin"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('pinned')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="unify.pinned"
|
||||
data-test-id="checkbox-pinned"
|
||||
switch
|
||||
:labels="checkboxLabel"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('config.label')"
|
||||
:description="$t('config.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="unify.config"
|
||||
data-test-id="textarea-config"
|
||||
:state="configState"
|
||||
rows="10"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4
|
||||
data-test-id="card-title"
|
||||
class="m-0"
|
||||
>
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:disabled="disabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', { unify, unifyAssets })"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<b-modal
|
||||
id="logo"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
body-class="p-1"
|
||||
>
|
||||
<b-img
|
||||
data-test-id="img-logo-preview"
|
||||
:src="unify.logo"
|
||||
fluid-grow
|
||||
/>
|
||||
</b-modal>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
name: 'CApplicationEditorUnify',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.applications',
|
||||
keyPrefix: 'editor.unify',
|
||||
},
|
||||
|
||||
props: {
|
||||
unify: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
application: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
canPin: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
unifyAssets: {
|
||||
icon: undefined,
|
||||
logo: undefined,
|
||||
},
|
||||
|
||||
checkboxLabel: {
|
||||
on: this.$t('general:label.general.yes'),
|
||||
off: this.$t('general:label.general.no'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
disabled () {
|
||||
return this.validConfig === false
|
||||
},
|
||||
|
||||
validConfig () {
|
||||
if (!this.unify) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
if ((this.unify.config || '').trim() !== '') {
|
||||
JSON.parse(this.unify.config)
|
||||
}
|
||||
return null
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
configState () {
|
||||
if (((this.unify || {}).config || '').trim() === '') {
|
||||
return null
|
||||
} else {
|
||||
return this.validConfig
|
||||
}
|
||||
},
|
||||
|
||||
showLogoPreview () {
|
||||
return this.unify.logoID !== NoID
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.unify.name = this.unify.name ? this.unify.name : this.application.name
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetLogo () {
|
||||
this.unify.logo = undefined
|
||||
this.unify.logoID = NoID
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
53
client/web/admin/src/components/Authclient/C3.js
Normal file
53
client/web/admin/src/components/Authclient/C3.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { default as component } from './CAuthclientEditorInfo.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
authclient: {
|
||||
authClientID: '',
|
||||
handle: 'corteza-webapp',
|
||||
meta: {
|
||||
description: '',
|
||||
name: 'Corteza Web Applications',
|
||||
},
|
||||
},
|
||||
roles: [],
|
||||
processing: false,
|
||||
success: false,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Editor info',
|
||||
group: ['AuthClient'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('Enable delete', {
|
||||
value (p) { return p.authclient.authClientID.length > 0 },
|
||||
update (p, val) { p.authclient.authClientID = val ? '123456789' : '' },
|
||||
}),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
authclient: {
|
||||
...props.authclient,
|
||||
handle: '',
|
||||
meta: {
|
||||
name: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
<template>
|
||||
<b-card
|
||||
v-if="resource"
|
||||
data-test-id="card-auth-client-info"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm auth-clients"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="resource.meta.name"
|
||||
data-test-id="input-name"
|
||||
required
|
||||
:state="nameState"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('handle.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="resource.handle"
|
||||
data-test-id="input-handle"
|
||||
:disabled="resource.isDefault"
|
||||
:placeholder="$t('handle.placeholder-handle')"
|
||||
:state="handleState"
|
||||
/>
|
||||
<b-form-invalid-feedback
|
||||
data-test-id="feedback-invalid-handle"
|
||||
:state="handleState"
|
||||
>
|
||||
{{ $t('handle.invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
|
||||
<template
|
||||
v-if="resource.isDefault"
|
||||
#description
|
||||
>
|
||||
{{ $t('handle.disabledFootnote') }}
|
||||
</template>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
:label="$t('redirectURI')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-form-table-wrapper
|
||||
:labels="{ addButton: $t('general:label.add') }"
|
||||
test-id="button-add-redirect-uris"
|
||||
@add-item="redirectURI.push('')"
|
||||
>
|
||||
<div
|
||||
v-if="redirectURI.length"
|
||||
>
|
||||
<b-input-group
|
||||
v-for="(value, index) in redirectURI"
|
||||
:key="index"
|
||||
class="mb-2"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="redirectURI[index]"
|
||||
data-test-id="input-uri"
|
||||
:placeholder="$t('uri')"
|
||||
/>
|
||||
<b-button
|
||||
data-test-id="button-remove-uri"
|
||||
class="ml-1 text-danger"
|
||||
variant="link"
|
||||
@click="redirectURI.splice(index, 1)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'times']"
|
||||
/>
|
||||
</b-button>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</c-form-table-wrapper>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
v-if="!fresh"
|
||||
:label="$t('secret')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('secret') }}
|
||||
<b-button
|
||||
v-if="!secretVisible"
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('tooltip.show-client-secret'), boundary: 'body' }"
|
||||
data-test-id="button-show-client-secret"
|
||||
variant="outline-light"
|
||||
size="sm"
|
||||
class="text-secondary border-0"
|
||||
@click="showSecret()"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'eye']"
|
||||
/>
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-else
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('tooltip.hide-client-secret'), boundary: 'body' }"
|
||||
data-test-id="button-hide-client-secret"
|
||||
variant="outline-light"
|
||||
size="sm"
|
||||
class="text-secondary border-0"
|
||||
@click="hideSecret()"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'eye-slash']"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model="secret"
|
||||
data-test-id="input-client-secret"
|
||||
disabled
|
||||
placeholder="****************************************************************"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('tooltip.regenerate-secret'), boundary: 'body' }"
|
||||
data-test-id="button-regenerate-client-secret"
|
||||
variant="outline-light"
|
||||
class="text-secondary border-0"
|
||||
@click="regenerateSecret()"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'sync']"
|
||||
/>
|
||||
</b-button>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group>
|
||||
<b-form-radio-group
|
||||
v-model="resource.validGrant"
|
||||
value="authorization_code"
|
||||
:options="[
|
||||
{ value: 'authorization_code', text: $t('grant.authorization_code') },
|
||||
{ value: 'client_credentials', text: $t('grant.client_credentials') },
|
||||
]"
|
||||
@change="onGrantChange"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
data-test-id="checkbox-allow-access-to-user-profile"
|
||||
:checked="((resource.scope || []).includes('profile'))"
|
||||
@change="setScope($event, 'profile')"
|
||||
>
|
||||
{{ $t('profile') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
data-test-id="checkbox-allow-access-to-corteza-api"
|
||||
:checked="((resource.scope || []).includes('api'))"
|
||||
@change="setScope($event, 'api')"
|
||||
>
|
||||
{{ $t('api') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
data-test-id="checkbox-allow-client-to-use-oidc"
|
||||
:checked="((resource.scope || []).includes('openid'))"
|
||||
@change="setScope($event, 'openid')"
|
||||
>
|
||||
{{ $t('openid') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-if="discoveryEnabled"
|
||||
data-test-id="checkbox-allow-client-access-to-discovery"
|
||||
:checked="((resource.scope || []).includes('discovery'))"
|
||||
@change="setScope($event, 'discovery')"
|
||||
>
|
||||
{{ $t('discovery') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
data-test-id="valid-from"
|
||||
:label="$t('validFrom.label')"
|
||||
:description="$t('validFrom.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-date-time
|
||||
v-model="resource.validFrom"
|
||||
data-test-id="input-valid-from"
|
||||
:labels="{
|
||||
clear: $t('general:label.clear'),
|
||||
none: $t('general:label.none'),
|
||||
now: $t('general:label.now'),
|
||||
today: $t('general:label.today'),
|
||||
}"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
data-test-id="expires-at"
|
||||
:label="$t('expiresAt.label')"
|
||||
:description="$t('expiresAt.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-date-time
|
||||
v-model="resource.expiresAt"
|
||||
data-test-id="input-expires-at"
|
||||
:labels="{
|
||||
clear: $t('general:label.clear'),
|
||||
none: $t('general:label.none'),
|
||||
now: $t('general:label.now'),
|
||||
today: $t('general:label.today'),
|
||||
}"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="resource.enabled"
|
||||
data-test-id="checkbox-is-client-enabled"
|
||||
:disabled="resource.isDefault"
|
||||
>
|
||||
{{ $t('enabled.label') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-text v-if="resource.isDefault">
|
||||
{{ $t('enabled.disabledFootnote') }}
|
||||
</b-form-text>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="resource.trusted"
|
||||
data-test-id="checkbox-is-client-trusted"
|
||||
>
|
||||
{{ $t('trusted.label') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-text>{{ $t('trusted.description') }}</b-form-text>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
v-show="isClientCredentialsGrant"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
data-test-id="impersonate-user"
|
||||
:label="$t('security.impersonateUser.label')"
|
||||
:description="$t('security.impersonateUser.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-user
|
||||
v-model="resource.security.impersonateUser"
|
||||
:placeholder="$t('security.impersonateUser.placeholder')"
|
||||
:clearable="true"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
data-test-id="permitted-roles"
|
||||
:label="$t('security.permittedRoles.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-role-picker
|
||||
v-model="resource.security.permittedRoles"
|
||||
>
|
||||
<template #description>
|
||||
{{ $t('security.permittedRoles.description') }}
|
||||
</template>
|
||||
</c-role-picker>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('security.prohibitedRoles.label')"
|
||||
data-test-id="prohibited-roles"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-role-picker
|
||||
v-model="resource.security.prohibitedRoles"
|
||||
>
|
||||
<template #description>
|
||||
{{ $t('security.prohibitedRoles.description') }}
|
||||
</template>
|
||||
</c-role-picker>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
data-test-id="forced-roles"
|
||||
:label="$t('security.forcedRoles.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-role-picker
|
||||
v-model="resource.security.forcedRoles"
|
||||
class="mb-3"
|
||||
>
|
||||
<template #description>
|
||||
{{ $t('security.forcedRoles.description') }}
|
||||
</template>
|
||||
</c-role-picker>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="!fresh && isClientCredentialsGrant"
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group label-class="text-primary">
|
||||
<template #label>
|
||||
{{ $t('cUrl') }}
|
||||
<b-button
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('tooltip.copy-cURL'), boundary: 'body' }"
|
||||
data-test-id="button-copy-cURL"
|
||||
variant="outline-light"
|
||||
size="sm"
|
||||
class="text-secondary border-0"
|
||||
@click="copyToClipboard(exampleCurl)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'copy']"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<b-textarea
|
||||
:value="exampleCurl"
|
||||
disabled
|
||||
data-test-id="cURL-string"
|
||||
rows="3"
|
||||
class="mb-2"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group label-class="text-primary">
|
||||
<template #label>
|
||||
{{ $t('accessToken') }}
|
||||
|
||||
<b-button
|
||||
v-if="tokenRequest.token"
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('tooltip.copy-access-token'), boundary: 'body' }"
|
||||
data-test-id="copy-token-from-request"
|
||||
variant="outline-light"
|
||||
size="sm"
|
||||
class="text-secondary border-0"
|
||||
@click="copyToClipboard(tokenRequest.token)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'copy']"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<b-textarea
|
||||
v-if="tokenRequest.token"
|
||||
data-test-id="cURL-string"
|
||||
:value="tokenRequest.token"
|
||||
disabled
|
||||
rows="5"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
v-else
|
||||
data-test-id="button-test-cURL"
|
||||
variant="light"
|
||||
@click="getAccessTokenAPI()"
|
||||
>
|
||||
{{ $t('generateAccessToken') }}
|
||||
</b-button>
|
||||
|
||||
<p
|
||||
v-if="tokenRequest.error"
|
||||
class="text-danger mt-2"
|
||||
>
|
||||
{{ tokenRequest.error }}
|
||||
</p>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('security.defaultUserGroup.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-user-group
|
||||
v-model="resource.security.userGroup"
|
||||
:placeholder="$t('security.defaultUserGroup.placeholder')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:resource="resource"
|
||||
/>
|
||||
|
||||
<!--
|
||||
include hidden input to enable
|
||||
trigger submit event w/ ENTER
|
||||
-->
|
||||
<input
|
||||
data-test-id="button-submit"
|
||||
type="submit"
|
||||
class="d-none"
|
||||
:disabled="saveDisabled"
|
||||
>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<template
|
||||
v-if="canDelete"
|
||||
>
|
||||
<c-input-confirm
|
||||
:data-test-id="isDeleted ? 'button-undelete': 'button-delete'"
|
||||
:disabled="processing"
|
||||
:text="isDeleted ? $t('undelete') : $t('delete')"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit(isDeleted ? 'undelete' : 'delete', resource.authClientID)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="submit"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
import { handle, components } from '@cortezaproject/corteza-vue'
|
||||
import CRolePicker from 'corteza-webapp-admin/src/components/CRolePicker'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import axios from 'axios'
|
||||
|
||||
const { CInputDateTime, CInputUser, CInputUserGroup } = components
|
||||
|
||||
export default {
|
||||
name: 'CAuthclientEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.authclients',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
components: {
|
||||
CRolePicker,
|
||||
CInputUser,
|
||||
CInputDateTime,
|
||||
CInputUserGroup,
|
||||
},
|
||||
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
canDelete: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
requestedSecret: '',
|
||||
secret: '',
|
||||
|
||||
redirectURI: this.resource.redirectURI ? this.resource.redirectURI.split(' ') : [],
|
||||
|
||||
curlVisible: false,
|
||||
|
||||
tokenRequest: {
|
||||
token: '',
|
||||
error: '',
|
||||
},
|
||||
|
||||
checkboxLabel: {
|
||||
on: this.$t('general:label.general.yes'),
|
||||
off: this.$t('general:label.general.no'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return !this.resource.authClientID || this.resource.authClientID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : this.resource.canUpdateAuthClient
|
||||
},
|
||||
|
||||
isDeleted () {
|
||||
return this.resource.deletedAt && this.resource.canDeleteAuthClient
|
||||
},
|
||||
|
||||
secretVisible () {
|
||||
return this.secret.length > 0
|
||||
},
|
||||
|
||||
nameState () {
|
||||
return this.resource.meta.name ? null : false
|
||||
},
|
||||
|
||||
handleState () {
|
||||
return handle.handleState(this.resource.handle)
|
||||
},
|
||||
|
||||
isClientCredentialsGrant () {
|
||||
return this.resource.validGrant === 'client_credentials'
|
||||
},
|
||||
|
||||
discoveryEnabled () {
|
||||
return this.$Settings.get('discovery.enabled', false)
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.nameState, this.handleState].includes(false)
|
||||
},
|
||||
|
||||
curlURL () {
|
||||
return this.$auth.cortezaAuthURL + '/oauth2/token'
|
||||
},
|
||||
|
||||
exampleCurl () {
|
||||
return `curl -X POST ${this.curlURL} -d grant_type=${this.resource.validGrant} -d scope='${this.resource.scope}' -u ${this.resource.authClientID}:${this.secret || 'PLACE-YOUR-CLIENT-SECRET-HERE'}`
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
redirectURI: {
|
||||
handler (redirectURI) {
|
||||
this.resource.redirectURI = redirectURI.filter(ru => ru).join(' ')
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onGrantChange (grant) {
|
||||
if (grant === 'client_credentials' && (!this.resource.security.impersonateUser || this.resource.security.impersonateUser === NoID)) {
|
||||
this.resource.security.impersonateUser = this.$auth.user.userID
|
||||
}
|
||||
},
|
||||
|
||||
copyToClipboard (value) {
|
||||
copy(value)
|
||||
},
|
||||
|
||||
toggleCurlSnippet () {
|
||||
this.curlVisible = !this.curlVisible
|
||||
},
|
||||
|
||||
submit () {
|
||||
if (!this.isClientCredentialsGrant || !this.resource.security.impersonateUser) {
|
||||
this.resource.security.impersonateUser = NoID
|
||||
}
|
||||
|
||||
this.$emit('submit', this.resource)
|
||||
},
|
||||
|
||||
setScope (value, target) {
|
||||
let items = this.resource.scope ? this.resource.scope.split(' ') : []
|
||||
|
||||
if (value) {
|
||||
items.push(target)
|
||||
} else {
|
||||
items = items.filter(i => i !== target)
|
||||
}
|
||||
|
||||
this.resource.scope = items.join(' ')
|
||||
},
|
||||
|
||||
requestSecret () {
|
||||
const clientID = this.resource.authClientID
|
||||
|
||||
return this.$SystemAPI.authClientExposeSecret(({ clientID })).then(secret => {
|
||||
this.requestedSecret = secret
|
||||
})
|
||||
},
|
||||
|
||||
async showSecret () {
|
||||
if (!this.requestedSecret) {
|
||||
await this.requestSecret()
|
||||
}
|
||||
|
||||
this.secret = this.requestedSecret
|
||||
},
|
||||
|
||||
hideSecret () {
|
||||
this.secret = ''
|
||||
},
|
||||
|
||||
async regenerateSecret () {
|
||||
const clientID = this.resource.authClientID
|
||||
|
||||
this.$SystemAPI.authClientRegenerateSecret(({ clientID })).then(secret => {
|
||||
this.requestedSecret = secret
|
||||
})
|
||||
},
|
||||
|
||||
async getAccessTokenAPI () {
|
||||
const clientID = this.resource.authClientID
|
||||
|
||||
if (!this.requestedSecret) {
|
||||
await this.requestSecret()
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
params.append('grant_type', this.resource.validGrant)
|
||||
params.append('scope', this.resource.scope)
|
||||
|
||||
axios.post(this.curlURL, params, { auth: { username: clientID, password: this.requestedSecret } }).then(response => {
|
||||
this.tokenRequest.token = (response.data || {}).access_token
|
||||
this.tokenRequest.error = ''
|
||||
}).catch(e => {
|
||||
const { error } = e.response.data || {}
|
||||
this.tokenRequest.error = error
|
||||
this.tokenRequest.token = ''
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
27
client/web/admin/src/components/C3.js
Normal file
27
client/web/admin/src/components/C3.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as applications from './Application/C3'
|
||||
import * as role from './Role/C3'
|
||||
import * as settings from './Settings/C3'
|
||||
|
||||
import { default as CAuthclientEditorInfo } from './Authclient/C3'
|
||||
import { default as CFederationEditorInfo } from './Federation/C3'
|
||||
import { default as CQueueEditorInfo } from './Queues/C3'
|
||||
import { default as CTemplateEditorInfo } from './Template/C3'
|
||||
import { default as CUserEditorInfo } from './User/C3'
|
||||
import { default as CWorkflowEditorInfo } from './Workflow/C3'
|
||||
import { default as CResourceListStatusFilter } from './CResourceListStatusFilter.c3'
|
||||
import { default as CSubmitButton } from './CSubmitButton.c3'
|
||||
|
||||
export default {
|
||||
...applications,
|
||||
...role,
|
||||
...settings,
|
||||
|
||||
CAuthclientEditorInfo,
|
||||
CFederationEditorInfo,
|
||||
CQueueEditorInfo,
|
||||
CTemplateEditorInfo,
|
||||
CUserEditorInfo,
|
||||
CWorkflowEditorInfo,
|
||||
CResourceListStatusFilter,
|
||||
CSubmitButton,
|
||||
}
|
||||
24
client/web/admin/src/components/CContentHeader.vue
Normal file
24
client/web/admin/src/components/CContentHeader.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex justify-content-end flex-wrap gap-1 mb-2 flex-fill-child"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ title }}
|
||||
</portal>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CContentHeader',
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
103
client/web/admin/src/components/CLocation.vue
Normal file
103
client/web/admin/src/components/CLocation.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-button
|
||||
v-if="label || editable"
|
||||
:variant="label ? 'link' : 'primary'"
|
||||
rounded
|
||||
:class="label ? 'p-0 border-0' : ''"
|
||||
@click="openMap"
|
||||
>
|
||||
<span v-if="label">
|
||||
{{ label }}
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'map-marked-alt']"
|
||||
/>
|
||||
{{ $t('openMap') }}
|
||||
</span>
|
||||
</b-button>
|
||||
|
||||
<b-modal
|
||||
v-model="map.show"
|
||||
size="xl"
|
||||
title="Map"
|
||||
body-class="p-0"
|
||||
hide-header
|
||||
hide-footer
|
||||
>
|
||||
<c-map
|
||||
:map="map"
|
||||
:markers="[{ value }]"
|
||||
style="height: 75vh; width: 100%;"
|
||||
@on-marker-click="removeMarker"
|
||||
@on-map-click="placeMarker"
|
||||
/>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CMap } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'general.label',
|
||||
},
|
||||
|
||||
components: {
|
||||
CMap,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
map: {
|
||||
show: false,
|
||||
zoom: 3,
|
||||
center: [30, 30],
|
||||
rotation: 0,
|
||||
attribution: '© <a target="_blank" rel="noopener noreferrer" href="http://osm.org/copyright">OpenStreetMap</a>',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openMap () {
|
||||
this.map.show = true
|
||||
},
|
||||
|
||||
placeMarker ({ latlng = {} }) {
|
||||
const { lat = 0, lng = 0 } = latlng
|
||||
this.$emit('input', [lat, lng])
|
||||
},
|
||||
|
||||
removeMarker () {
|
||||
this.$emit('input', [])
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
127
client/web/admin/src/components/CMemberPicker.vue
Normal file
127
client/web/admin/src/components/CMemberPicker.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div
|
||||
data-test-id="member-picker"
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<c-input-user
|
||||
data-test-id="input-member-picker"
|
||||
:selectable="r => !value.includes(r.userID)"
|
||||
:placeholder="$t('admin:picker.member.placeholder')"
|
||||
clear-on-select
|
||||
@input-object="addUser"
|
||||
/>
|
||||
|
||||
<b-spinner
|
||||
v-if="preloading"
|
||||
class="mx-auto my-4"
|
||||
/>
|
||||
|
||||
<b-table-simple
|
||||
v-else-if="getSelectedUsers.length"
|
||||
responsive
|
||||
small
|
||||
hover
|
||||
class="w-100 p-0 mb-0 mt-1"
|
||||
>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="user in getSelectedUsers"
|
||||
:key="user.userID"
|
||||
data-test-id="selected-row-list"
|
||||
>
|
||||
<td class="align-middle">
|
||||
<c-user-label :user="user" />
|
||||
</td>
|
||||
<td
|
||||
v-if="!noRemove"
|
||||
class="text-right"
|
||||
>
|
||||
<c-input-confirm
|
||||
data-test-id="button-remove-user"
|
||||
show-icon
|
||||
@confirmed="removeUser(user.userID)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</b-table-simple>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputUser, CUserLabel } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CInputUser,
|
||||
CUserLabel,
|
||||
},
|
||||
|
||||
props: {
|
||||
// list of user IDs
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
noRemove: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
fetching: false,
|
||||
preloading: false,
|
||||
|
||||
filter: '',
|
||||
|
||||
selectedUsers: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
getSelectedUsers () {
|
||||
return this.selectedUsers.filter(({ userID }) => this.value.includes(userID))
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.preloadSelected()
|
||||
},
|
||||
|
||||
methods: {
|
||||
addUser (user) {
|
||||
if (!this.value.includes(user.userID)) {
|
||||
this.selectedUsers.push(user)
|
||||
this.$emit('input', [...this.value, user.userID])
|
||||
}
|
||||
},
|
||||
|
||||
removeUser (userID) {
|
||||
this.selectedUsers = this.selectedUsers.filter(({ userID: rID }) => rID !== userID)
|
||||
this.$emit('input', this.value.filter(v => v !== userID))
|
||||
},
|
||||
|
||||
preloadSelected () {
|
||||
if (!this.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
this.preloading = true
|
||||
|
||||
return this.$SystemAPI.userList({ userID: this.value, suspended: 1, deleted: 1 })
|
||||
.then(({ set }) => {
|
||||
this.selectedUsers = set || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.preloading = false
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:user.fetch.error')))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { default as component } from './CResourceListStatusFilter'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { input, select } = components.C3.controls
|
||||
|
||||
const options = [
|
||||
{ value: '0', text: 'Excluded' },
|
||||
{ value: '1', text: 'Inclusive' },
|
||||
{ value: '2', text: 'Exclusive' },
|
||||
]
|
||||
|
||||
const controls = [
|
||||
[
|
||||
input('Label', 'label'),
|
||||
select('Filter', 'value', options),
|
||||
],
|
||||
[
|
||||
input('Excluded label', 'excluded-label'),
|
||||
input('Inclusive label', 'inclusive-label'),
|
||||
input('Exclusive label', 'exclusive-label'),
|
||||
],
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'Resource list status filter',
|
||||
group: ['Root components'],
|
||||
component,
|
||||
controls,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<b-col class="d-flex align-items-center flex-wrap gap-1">
|
||||
<b-form-radio-group
|
||||
v-model="$attrs.value"
|
||||
v-bind="$attrs"
|
||||
:options="[
|
||||
{ value: 0, text: $attrs['excluded-label'] || 'excluded' },
|
||||
{ value: 1, text: $attrs['inclusive-label'] || 'inclusive' },
|
||||
{ value: 2, text: $attrs['exclusive-label'] || 'exclusive' }
|
||||
]"
|
||||
buttons
|
||||
button-variant="outline-primary"
|
||||
size="sm"
|
||||
name="radio-btn-outline"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<span class="text-nowrap">
|
||||
{{ $attrs.label }}
|
||||
</span>
|
||||
</b-col>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CResourceListStatusFilter',
|
||||
}
|
||||
</script>
|
||||
139
client/web/admin/src/components/CRolePicker.vue
Normal file
139
client/web/admin/src/components/CRolePicker.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div
|
||||
data-test-id="role-picker"
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<c-input-role
|
||||
data-test-id="input-role-picker"
|
||||
:selectable="r => !value.includes(r.roleID)"
|
||||
:placeholder="$t('admin:picker.role.placeholder')"
|
||||
:visible="isRoleVisible"
|
||||
clear-on-select
|
||||
@input="addRole($event)"
|
||||
/>
|
||||
|
||||
<b-spinner
|
||||
v-if="preloading"
|
||||
class="mx-auto my-4"
|
||||
/>
|
||||
|
||||
<b-table-simple
|
||||
v-else-if="getSelectedRoles.length"
|
||||
responsive
|
||||
small
|
||||
hover
|
||||
class="w-100 p-0 mb-0 mt-1"
|
||||
>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="role in getSelectedRoles"
|
||||
:key="role.roleID"
|
||||
data-test-id="selected-row-list"
|
||||
>
|
||||
<td class="align-middle">
|
||||
{{ getRoleLabel(role) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<c-input-confirm
|
||||
data-test-id="button-remove-role"
|
||||
show-icon
|
||||
@confirmed="removeRole(role.roleID)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</b-table-simple>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputRole } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CInputRole,
|
||||
},
|
||||
|
||||
props: {
|
||||
// list of role IDs
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
fetching: false,
|
||||
preloading: false,
|
||||
|
||||
filter: '',
|
||||
|
||||
selectedRoles: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
getSelectedRoles () {
|
||||
return this.selectedRoles.filter(({ roleID }) => this.value.includes(roleID))
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.preloadSelected()
|
||||
},
|
||||
|
||||
methods: {
|
||||
addRole (role) {
|
||||
if (!this.value.includes(role.roleID)) {
|
||||
this.selectedRoles.push(role)
|
||||
this.$emit('input', [...this.value, role.roleID])
|
||||
}
|
||||
},
|
||||
|
||||
removeRole (roleID) {
|
||||
this.selectedRoles = this.selectedRoles.filter(({ roleID: rID }) => rID !== roleID)
|
||||
this.$emit('input', this.value.filter(v => v !== roleID))
|
||||
},
|
||||
|
||||
preloadSelected () {
|
||||
if (!this.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
this.preloading = true
|
||||
|
||||
return this.$SystemAPI.roleList({ roleID: this.value })
|
||||
.then(({ set }) => {
|
||||
this.selectedRoles = set || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.preloading = false
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:role.fetch.error')))
|
||||
},
|
||||
|
||||
getRoleLabel ({ name, handle, roleID }) {
|
||||
return name || handle || roleID
|
||||
},
|
||||
|
||||
isRoleVisible ({ isClosed, meta = {} }) {
|
||||
return !(isClosed || (meta.context && meta.context.resourceTypes))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.results {
|
||||
z-index: 100;
|
||||
.filtered-role {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
157
client/web/admin/src/components/CSystemFields.vue
Normal file
157
client/web/admin/src/components/CSystemFields.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<b-row>
|
||||
<b-col
|
||||
v-if="showDivider"
|
||||
cols="12"
|
||||
>
|
||||
<hr>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="hasID"
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('id')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ id }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-for="(f, i) in systemFields"
|
||||
:key="i"
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="getFieldValue(f) !== '0'"
|
||||
:label="$t(`general:system.field.${f}`)"
|
||||
label-class="text-primary"
|
||||
:data-test-id="`input-${generateTestID(f)}`"
|
||||
>
|
||||
<c-user-label
|
||||
v-if="isUserField(f)"
|
||||
:user="getUserForField(f)"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ getFieldValue(f) }}
|
||||
</template>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<slot name="custom-field" />
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import { getSystemFields, kebabize, userFields } from 'corteza-webapp-admin/src/lib/sysFields'
|
||||
|
||||
const { CUserLabel } = components
|
||||
|
||||
const userCache = new Map()
|
||||
const inflight = new Map()
|
||||
|
||||
export default {
|
||||
name: 'CSystemFields',
|
||||
|
||||
components: {
|
||||
CUserLabel,
|
||||
},
|
||||
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
resolvedUsers: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
systemFields () {
|
||||
return getSystemFields(this.resource)
|
||||
},
|
||||
|
||||
hasID () {
|
||||
return this.id && this.id !== '0'
|
||||
},
|
||||
|
||||
hasVisibleSystemFields () {
|
||||
return this.systemFields.some(f => this.getFieldValue(f) !== '0')
|
||||
},
|
||||
|
||||
showDivider () {
|
||||
return this.hasID || this.hasVisibleSystemFields
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
resource: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler () {
|
||||
userFields.forEach(f => {
|
||||
const userID = this.resource[f]
|
||||
if (userID && userID !== '0') {
|
||||
this.resolveUser(userID)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
generateTestID (field) {
|
||||
return kebabize(field)
|
||||
},
|
||||
|
||||
isUserField (field) {
|
||||
return userFields.includes(field)
|
||||
},
|
||||
|
||||
getFieldValue (field) {
|
||||
const isTimeValue = field.substring(field.length - 2) === 'At'
|
||||
return isTimeValue ? this.$options.filters.locFullDateTime(this.resource[field]) : this.resource[field]
|
||||
},
|
||||
|
||||
getUserForField (field) {
|
||||
const userID = this.resource[field]
|
||||
if (!userID || userID === '0') return {}
|
||||
return this.resolvedUsers[userID] || { userID }
|
||||
},
|
||||
|
||||
resolveUser (userID) {
|
||||
if (this.resolvedUsers[userID]) return
|
||||
if (userCache.has(userID)) {
|
||||
Vue.set(this.resolvedUsers, userID, 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 => {
|
||||
Vue.set(this.resolvedUsers, userID, user)
|
||||
}).catch(() => {
|
||||
Vue.set(this.resolvedUsers, userID, { userID })
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
330
client/web/admin/src/components/CTheMainNav.vue
Normal file
330
client/web/admin/src/components/CTheMainNav.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-auto py-3"
|
||||
>
|
||||
<b-list-group
|
||||
v-for="(grp, g) in navigation"
|
||||
:key="g"
|
||||
tag="li"
|
||||
>
|
||||
<h6
|
||||
v-if="grp.header"
|
||||
class="mt-3 text-uppercase"
|
||||
>
|
||||
{{ $t(grp.header.label) }}
|
||||
</h6>
|
||||
|
||||
<c-sidebar-nav-items
|
||||
:items="grp.items"
|
||||
default-route-name="dashboard"
|
||||
class="overflow-auto h-100"
|
||||
/>
|
||||
</b-list-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CSidebarNavItems } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CSidebarNavItems,
|
||||
},
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'navigation',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
nav: [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
label: 'dashboard',
|
||||
route: 'dashboard',
|
||||
icon: 'tachometer-alt',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: { label: 'system.group' },
|
||||
items: [
|
||||
{
|
||||
label: 'system.items.users',
|
||||
route: 'system.user',
|
||||
icon: 'users',
|
||||
can: [
|
||||
['system/', 'users.search'],
|
||||
['system/', 'user.create'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.roles',
|
||||
route: 'system.role',
|
||||
icon: 'hat-cowboy',
|
||||
can: [
|
||||
['system/', 'roles.search'],
|
||||
['system/', 'role.create'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.usergroups',
|
||||
route: 'system.userGroup',
|
||||
icon: 'user-group',
|
||||
can: ['system/', 'user-groups.search'],
|
||||
},
|
||||
{
|
||||
label: 'system.items.applications',
|
||||
route: 'system.application',
|
||||
icon: 'th-large',
|
||||
can: [
|
||||
['system/', 'applications.search'],
|
||||
['system/', 'application.create'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.templates',
|
||||
route: 'system.template',
|
||||
icon: 'file-code',
|
||||
can: ['system/', 'templates.search'],
|
||||
},
|
||||
{
|
||||
label: 'system.items.settings',
|
||||
route: 'system.settings',
|
||||
icon: 'sliders-h',
|
||||
can: ['system/', 'settings.read'],
|
||||
},
|
||||
{
|
||||
label: 'system.items.email',
|
||||
route: 'system.email',
|
||||
icon: 'envelope-open',
|
||||
// all email management is handled
|
||||
// via settings for now
|
||||
can: [
|
||||
['system/', 'settings.read'],
|
||||
['system/', 'settings.manage'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.authclients',
|
||||
route: 'system.authClient',
|
||||
icon: 'key',
|
||||
can: [
|
||||
['system/', 'auth-clients.search'],
|
||||
['system/', 'auth-client.create'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.actionlog',
|
||||
route: 'system.actionlog',
|
||||
icon: 'glasses',
|
||||
can: ['system/', 'action-log.read'],
|
||||
},
|
||||
{
|
||||
label: 'system.items.queues',
|
||||
route: 'system.queue',
|
||||
icon: 'stream',
|
||||
can: [
|
||||
['system/', 'queues.search'],
|
||||
['system/', 'queue.create'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.apigw',
|
||||
route: 'system.apigw',
|
||||
icon: 'archway',
|
||||
can: [
|
||||
['system/', 'apigw-routes.search'],
|
||||
['system/', 'apigw-route.create'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.connections',
|
||||
route: 'system.connection',
|
||||
icon: 'cloud',
|
||||
can: [
|
||||
['system/', 'dal-connections.search'],
|
||||
['system/', 'dal-connection.create'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.code-snippets',
|
||||
route: 'system.codesnippets',
|
||||
icon: 'file-code',
|
||||
can: [
|
||||
['system/', 'settings.read'],
|
||||
['system/', 'settings.manage'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'system.items.sensitivityLevel',
|
||||
route: 'system.sensitivityLevel',
|
||||
icon: 'stamp',
|
||||
can: ['system/', 'dal-sensitivity-level.manage'],
|
||||
},
|
||||
{
|
||||
label: 'system.items.permissions',
|
||||
route: 'system.permissions',
|
||||
icon: 'lock',
|
||||
can: ['system/', 'grant'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
header: { label: 'compose.group' },
|
||||
items: [
|
||||
{
|
||||
label: 'compose.items.settings',
|
||||
route: 'compose.settings',
|
||||
icon: 'sliders-h',
|
||||
can: [
|
||||
['compose/', 'settings.read'],
|
||||
['compose/', 'settings.manage'],
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
label: 'compose.items.permissions',
|
||||
route: 'compose.permissions',
|
||||
icon: 'lock',
|
||||
can: ['compose/', 'grant'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
header: { label: 'automation.group' },
|
||||
items: [
|
||||
{
|
||||
label: 'automation.items.workflows',
|
||||
route: 'automation.workflow',
|
||||
icon: 'project-diagram',
|
||||
can: [
|
||||
['automation/', 'workflows.search'],
|
||||
['automation/', 'workflow.create'],
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
label: 'automation.items.sessions',
|
||||
route: 'automation.session',
|
||||
icon: 'business-time',
|
||||
can: ['automation/', 'sessions.search'],
|
||||
},
|
||||
|
||||
{
|
||||
label: 'automation.items.scripts',
|
||||
route: 'automation.scripts',
|
||||
icon: 'scroll',
|
||||
can: ['automation/', 'workflows.search'],
|
||||
},
|
||||
|
||||
{
|
||||
label: 'automation.items.permissions',
|
||||
route: 'automation.permissions',
|
||||
icon: 'lock',
|
||||
can: ['automation/', 'grant'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: { label: 'federation.group' },
|
||||
items: [
|
||||
{
|
||||
label: 'federation.items.nodes',
|
||||
route: 'federation.nodes',
|
||||
icon: 'share-alt',
|
||||
can: ['federation/', 'pair'],
|
||||
},
|
||||
{
|
||||
label: 'federation.items.permissions',
|
||||
route: 'federation.permissions',
|
||||
icon: 'lock',
|
||||
can: ['federation/', 'grant'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: { label: 'ui.group' },
|
||||
items: [
|
||||
{
|
||||
label: 'ui.items.theming',
|
||||
route: 'theming.settings',
|
||||
icon: 'palette',
|
||||
can: [
|
||||
['system/', 'settings.read'],
|
||||
['system/', 'settings.manage'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ui.items.navigation',
|
||||
route: 'navigation.settings',
|
||||
icon: 'bars',
|
||||
can: [
|
||||
['system/', 'settings.read'],
|
||||
['system/', 'settings.manage'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ui.items.location',
|
||||
route: 'location.settings',
|
||||
icon: 'map-marker-alt',
|
||||
can: [
|
||||
['system/', 'settings.read'],
|
||||
['system/', 'settings.manage'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
can: 'rbac/can',
|
||||
}),
|
||||
|
||||
navigation () {
|
||||
return this.nav.map(grp => {
|
||||
grp = JSON.parse(JSON.stringify(grp))
|
||||
grp.items = grp.items
|
||||
.map(itm => {
|
||||
const page = {
|
||||
name: itm.route,
|
||||
title: this.$t(itm.label),
|
||||
icon: ['fas', itm.icon],
|
||||
}
|
||||
|
||||
if (!itm.can) {
|
||||
// if not explicitly set, allow
|
||||
itm.can = true
|
||||
}
|
||||
|
||||
if (Array.isArray(itm.can)) {
|
||||
// if array then call the perm checking function
|
||||
if (Array.isArray(itm.can[0])) {
|
||||
itm.can = itm.can.every(c => this.can(...c))
|
||||
} else {
|
||||
itm.can = this.can(...itm.can)
|
||||
}
|
||||
}
|
||||
|
||||
return { page, can: itm.can }
|
||||
})
|
||||
.filter(({ can }) => can)
|
||||
|
||||
if (grp.items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return grp
|
||||
}).filter(i => i)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
84
client/web/admin/src/components/CUploaderWithPreview.vue
Normal file
84
client/web/admin/src/components/CUploaderWithPreview.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<b-form
|
||||
@submit.prevent="$emit('upload')"
|
||||
>
|
||||
<c-uploader
|
||||
v-if="!disabled"
|
||||
:endpoint="uploadEndpoint"
|
||||
:labels="{
|
||||
placeholder: $t('general:label.dropFiles'),
|
||||
uploading: $t('general:label.uploading'),
|
||||
fileTypeNotAllowed: $t('general:label.fileTypeNotAllowed'),
|
||||
}"
|
||||
:accepted-files="['image/*']"
|
||||
@upload="$emit('upload', $event)"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="value"
|
||||
class="d-flex justify-content-center w-100 mt-2"
|
||||
>
|
||||
<b-img
|
||||
:src="value"
|
||||
class="mw-100 h-auto"
|
||||
/>
|
||||
</div>
|
||||
</b-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CUploader } = components
|
||||
|
||||
export default {
|
||||
name: 'CUploaderWithPreview',
|
||||
|
||||
components: {
|
||||
CUploader,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: () => undefined,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
|
||||
labels: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
acceptedFiles: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
maxFilesize: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
panels: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
uploadEndpoint () {
|
||||
return this.$SystemAPI.baseURL + this.endpoint
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-row
|
||||
v-if="issues.length"
|
||||
>
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
:label="$t('connectivity-issues')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-alert
|
||||
v-for="issue in issues"
|
||||
:key="issue.issue"
|
||||
show
|
||||
variant="danger"
|
||||
>
|
||||
{{ issue.issue }}
|
||||
</b-alert>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
:label="$t('form.model-ident.label')"
|
||||
:description="$t('form.model-ident.description', { interpolation: { prefix: '{{{', suffix: '}}}' } })"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="dal.modelIdent"
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('form.model-ident.placeholder')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
:label="$t('form.type.label')"
|
||||
:description="$t('form.type.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="dal.type"
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('form.type.placeholder')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
:label="$t('form.params.label')"
|
||||
:description="$t('form.params.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="paramsJson"
|
||||
:disabled="disabled"
|
||||
:class="paramsJsonEditorClass"
|
||||
:placeholder="$t('form.params.placeholder')"
|
||||
rows="5"
|
||||
@blur="processParamsJSON"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:disabled="disabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.connections',
|
||||
keyPrefix: 'editor.dal',
|
||||
},
|
||||
|
||||
props: {
|
||||
disabled: { type: Boolean, default: false },
|
||||
|
||||
dal: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
issues: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
/**
|
||||
* JSON string version of the connection params object
|
||||
* used to display nicely formatted JSON in the textarea
|
||||
*/
|
||||
paramsJson: '',
|
||||
|
||||
// holds JSON validation errors
|
||||
paramsJsonEditorClass: '',
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
dal: {
|
||||
handler: function (dal) {
|
||||
this.paramsJson = JSON.stringify(dal.params || { dsn: '' }, null, 2)
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Validates JSON string and sets errors if any
|
||||
* In case of a valid JSON it will update the connection object
|
||||
*/
|
||||
processParamsJSON () {
|
||||
this.paramsJsonEditorClass = ''
|
||||
|
||||
try {
|
||||
// parse json string, ensure it's an object
|
||||
const json = JSON.parse(this.paramsJson)
|
||||
if (typeof json !== 'object') {
|
||||
throw new Error('JSON is not an object')
|
||||
}
|
||||
|
||||
// iterate through all properties and assign them to connection object
|
||||
if (!this.dal.params) {
|
||||
this.$set(this.dal, 'params', {})
|
||||
}
|
||||
for (const key in json) {
|
||||
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
||||
this.$set(this.dal.params, key, json[key])
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.paramsJsonEditorClass = 'border-danger'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-connection-settings"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('form.name.label')"
|
||||
:description="$t('form.name.description')"
|
||||
label-class="text-primary"
|
||||
class="mb-3"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="connection.meta.name"
|
||||
required
|
||||
:placeholder="$t('form.name.placeholder')"
|
||||
:state="nameState"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('form.handle.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-3"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="connection.handle"
|
||||
:disabled="isPrimary || disabled"
|
||||
:placeholder="$t('form.handle.placeholder')"
|
||||
:state="handleState"
|
||||
/>
|
||||
<b-form-invalid-feedback :state="handleState">
|
||||
{{ $t('form.handle.invalid-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('form.location-name.label')"
|
||||
:description="$t('form.location-name.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="connection.meta.location.properties.name"
|
||||
:placeholder="$t('form.location-name.placeholder')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('form.location-geometry.label')"
|
||||
:description="$t('form.location-geometry.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-location
|
||||
v-if="!disabled"
|
||||
v-model="locationCoordinates"
|
||||
:label="locationCoordinatesLabel"
|
||||
editable
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('form.ownership.label')"
|
||||
:description="$t('form.ownership.description')"
|
||||
label-class="text-primary"
|
||||
class="mb-3"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="connection.meta.ownership"
|
||||
:placeholder="$t('form.ownership.placeholder')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('form.sensitivity-level.label')"
|
||||
:description="$t('form.sensitivity-level.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-sensitivity-level-picker
|
||||
v-model="connection.config.privacy.sensitivityLevelID"
|
||||
:options="sensitivityLevels"
|
||||
:placeholder="$t('form.sensitivity-level.placeholder')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="!fresh && !isPrimary && !disabled"
|
||||
:text="connection.deletedAt ? $t('general:label.undelete') : $t('general:label.delete')"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="disabled || saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
import { components, handle } from '@cortezaproject/corteza-vue'
|
||||
import CLocation from 'corteza-webapp-admin/src/components/CLocation'
|
||||
const { CSensitivityLevelPicker } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.connections',
|
||||
keyPrefix: 'editor.basic',
|
||||
},
|
||||
|
||||
components: {
|
||||
CLocation,
|
||||
CSensitivityLevelPicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
connection: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
sensitivityLevels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isPrimary () {
|
||||
return this.connection.type === 'corteza::system:primary-dal-connection'
|
||||
},
|
||||
|
||||
fresh () {
|
||||
return !this.connection.connectionID || this.connection.connectionID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : true
|
||||
},
|
||||
|
||||
nameState () {
|
||||
return this.connection.meta.name ? null : false
|
||||
},
|
||||
|
||||
handleState () {
|
||||
return handle.handleState(this.connection.handle)
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.nameState, this.handleState].includes(false)
|
||||
},
|
||||
|
||||
locationCoordinates: {
|
||||
get () {
|
||||
const { coordinates = [] } = (this.connection.meta.location || {}).geometry || {}
|
||||
return coordinates || []
|
||||
},
|
||||
|
||||
set (value) {
|
||||
if (!this.connection.meta.location) {
|
||||
this.$set(this.connection.meta, 'location', { type: 'Feature', geometry: { type: 'Point', coordinates: [] }, properties: { name: '' } })
|
||||
}
|
||||
|
||||
if (!this.connection.meta.location.geometry) {
|
||||
this.$set(this.connection.meta.location, 'geometry', { type: 'Point', coordinates: [] })
|
||||
}
|
||||
|
||||
this.connection.meta.location.geometry.coordinates = value
|
||||
},
|
||||
},
|
||||
|
||||
locationCoordinatesLabel () {
|
||||
return (this.locationCoordinates || []).map(c => c?.toFixed?.(7) || c).join(', ')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-row
|
||||
v-for="prop in list"
|
||||
:key="prop"
|
||||
>
|
||||
<b-col cols="12">
|
||||
<b-form-checkbox
|
||||
v-model="properties[prop].enabled"
|
||||
class="mb-1"
|
||||
>
|
||||
{{ $t('form.' + kebabCase(prop) + '.checkbox.label') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('form.' + kebabCase(prop) + '.notes.label')"
|
||||
:description="$t('form.' + kebabCase(prop) + '.notes.description')"
|
||||
label-class="text-primary"
|
||||
class="ml-4"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="properties[prop].notes"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:disabled="disabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
<script>
|
||||
import { kebabCase } from 'lodash'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.connections',
|
||||
keyPrefix: 'editor.properties',
|
||||
},
|
||||
|
||||
props: {
|
||||
properties: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
list: [
|
||||
'dataAtRestEncryption',
|
||||
'dataAtRestProtection',
|
||||
'dataAtTransitEncryption',
|
||||
'dataRestoration',
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
kebabCase (str) {
|
||||
return kebabCase(str)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-primary-database"
|
||||
class="shadow-sm"
|
||||
header-class="border-bottom"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="d-flex align-items-center gap-1 mb-0">
|
||||
{{ $t('title') }}
|
||||
|
||||
<b-button
|
||||
v-if="connection"
|
||||
data-test-id="button-edit"
|
||||
size="sm"
|
||||
variant="outline-light"
|
||||
class="d-flex align-items-center text-primary border-0 p-1"
|
||||
:to="{ name: 'system.connection.edit', params: { connectionID: (connection || {}).connectionID } }"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'edit']"
|
||||
/>
|
||||
</b-button>
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="connection"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
class="mb-3"
|
||||
>
|
||||
{{ (connection.meta || {}).name }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('handle')"
|
||||
label-class="text-primary"
|
||||
class="mb-3"
|
||||
>
|
||||
{{ connection.handle }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('location')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-location
|
||||
:value="locationCoordinates"
|
||||
:label="locationName || locationCoordinatesLabel"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('ownership')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ (connection.meta || {}).ownership }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('sensitivity-level')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ sensitivityLevelName }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CLocation from 'corteza-webapp-admin/src/components/CLocation'
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CLocation,
|
||||
},
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.connections',
|
||||
keyPrefix: 'primary',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
|
||||
connection: undefined,
|
||||
sensitivityLevel: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
locationCoordinates () {
|
||||
const { coordinates = [] } = (this.connection.meta || {}).location?.geometry || {}
|
||||
|
||||
return coordinates || []
|
||||
},
|
||||
|
||||
locationCoordinatesLabel () {
|
||||
return (this.locationCoordinates || []).map(c => c?.toFixed?.(7) || c).join(', ')
|
||||
},
|
||||
|
||||
locationName () {
|
||||
return (this.connection.meta || {}).location?.properties?.name
|
||||
},
|
||||
|
||||
sensitivityLevelName () {
|
||||
const { sensitivityLevelID, handle, meta = {} } = this.sensitivityLevel || {}
|
||||
return meta.name || handle || sensitivityLevelID || 'N/A'
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchPrimaryConnection()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchPrimaryConnection () {
|
||||
this.loading = true
|
||||
|
||||
return this.$SystemAPI.dalConnectionList({ type: 'corteza::system:primary-dal-connection' }).then(({ set = [] }) => {
|
||||
this.connection = set.find(({ type }) => type === 'corteza::system:primary-dal-connection')
|
||||
|
||||
if (!this.connection) {
|
||||
return
|
||||
}
|
||||
|
||||
const { sensitivityLevelID } = (this.connection.config || {}).privacy || {}
|
||||
|
||||
if (sensitivityLevelID && sensitivityLevelID !== NoID) {
|
||||
return this.$SystemAPI.dalSensitivityLevelRead({ sensitivityLevelID })
|
||||
.then(sensitivityLevel => {
|
||||
this.sensitivityLevel = sensitivityLevel
|
||||
})
|
||||
}
|
||||
}).catch(this.toastErrorHandler(this.$t('notification:connection.fetch.error')))
|
||||
.finally(async () => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
body-class="p-0"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template
|
||||
#header
|
||||
>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<c-resource-list
|
||||
:primary-key="primaryKey"
|
||||
:filter="filter"
|
||||
:sorting="sorting"
|
||||
:pagination="pagination"
|
||||
:fields="fields"
|
||||
:items="items"
|
||||
:row-class="genericRowClass"
|
||||
:translations="{
|
||||
notFound: $t('admin:general.notFound'),
|
||||
noItems: $t('admin:general.resource-list.no-items'),
|
||||
loading: $t('loading'),
|
||||
showingPagination: 'admin:general.pagination.showing',
|
||||
singlePluralPagination: 'admin:general.pagination.single',
|
||||
prevPagination: $t('admin:general.pagination.prev'),
|
||||
nextPagination: $t('admin:general.pagination.next'),
|
||||
resourceSingle: $t('general:label.connection.single'),
|
||||
resourcePlural: $t('general:label.connection.plural')
|
||||
}"
|
||||
clickable
|
||||
hide-search
|
||||
class="h-100 bg-transparent"
|
||||
@row-clicked="handleRowClicked"
|
||||
>
|
||||
<template #header>
|
||||
<b-button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
:to="{ name: 'system.connection.new' }"
|
||||
>
|
||||
{{ $t('add-button') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #toolbar>
|
||||
<c-resource-list-status-filter
|
||||
v-model="filter.deleted"
|
||||
data-test-id="filter-deleted-connections"
|
||||
:label="$t('filterForm.deleted.label')"
|
||||
:excluded-label="$t('filterForm.excluded.label')"
|
||||
:inclusive-label="$t('filterForm.inclusive.label')"
|
||||
:exclusive-label="$t('filterForm.exclusive.label')"
|
||||
@change="filterList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions="{ item: c }">
|
||||
<b-dropdown
|
||||
v-if="c.canDeleteConnection"
|
||||
variant="outline-extra-light"
|
||||
toggle-class="d-flex align-items-center justify-content-center text-primary border-0 py-2"
|
||||
no-caret
|
||||
dropleft
|
||||
lazy
|
||||
menu-class="m-0"
|
||||
>
|
||||
<template #button-content>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'ellipsis-v']"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<c-input-confirm
|
||||
:text="getActionText(c)"
|
||||
show-icon
|
||||
:icon="getActionIcon(c)"
|
||||
borderless
|
||||
variant="link"
|
||||
size="md"
|
||||
button-class="dropdown-item"
|
||||
icon-class="text-danger"
|
||||
class="w-100"
|
||||
@confirmed="handleDelete(c)"
|
||||
/>
|
||||
</b-dropdown>
|
||||
</template>
|
||||
</c-resource-list>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import listHelpers from 'corteza-webapp-admin/src/mixins/listHelpers'
|
||||
import moment from 'moment'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CResourceList } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CResourceList,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
listHelpers,
|
||||
],
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.connections',
|
||||
keyPrefix: 'list',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
id: 'connections',
|
||||
|
||||
primaryKey: 'connectionID',
|
||||
editRoute: 'system.connection.edit',
|
||||
|
||||
filter: {
|
||||
type: 'corteza::system:dal-connection',
|
||||
query: '',
|
||||
deleted: 0,
|
||||
},
|
||||
|
||||
sorting: {
|
||||
sortBy: 'createdAt',
|
||||
sortDesc: true,
|
||||
},
|
||||
|
||||
fields: [
|
||||
{
|
||||
key: 'name',
|
||||
sortable: false,
|
||||
formatter: (value, col, conn) => conn.meta.name || conn.handle,
|
||||
},
|
||||
{
|
||||
key: 'location',
|
||||
sortable: false,
|
||||
formatter: (value, col, conn) => conn.meta.location.properties.name,
|
||||
},
|
||||
{
|
||||
key: 'ownership',
|
||||
sortable: false,
|
||||
formatter: (value, col, conn) => conn.meta.ownership,
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
sortable: false,
|
||||
formatter: (v) => moment(v).fromNow(),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
class: 'actions',
|
||||
},
|
||||
].map(c => ({
|
||||
// Generate column label translation key
|
||||
label: c.label || this.$t(`columns.${c.key}`),
|
||||
...c,
|
||||
})),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
items () {
|
||||
return this.procListResults(this.$SystemAPI.dalConnectionListCancellable(this.encodeListParams()))
|
||||
},
|
||||
|
||||
handleDelete (connection) {
|
||||
this.handleItemDelete({
|
||||
resource: connection,
|
||||
resourceName: 'dalConnection',
|
||||
locale: 'connection',
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<b-card
|
||||
class="shadow-sm"
|
||||
body-class="p-0"
|
||||
>
|
||||
<template
|
||||
#header
|
||||
>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<c-resource-list
|
||||
primary-key="federationID"
|
||||
:loading-text="$t('loading')"
|
||||
:pagination="pagination"
|
||||
:sorting="sorting"
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
>
|
||||
<template #actions>
|
||||
<b-button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
:to="{ name: 'system.connections.new' }"
|
||||
>
|
||||
{{ $t('add-button') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #filter>
|
||||
<b-input-group
|
||||
class="h-100"
|
||||
>
|
||||
<b-form-input
|
||||
v-model.trim="filter.query"
|
||||
:placeholder="$t('filterForm.query.placeholder')"
|
||||
class="text-truncate border-right-0 h-100"
|
||||
@search="filterList"
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text class="text-primary bg-white">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'search']"
|
||||
/>
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</template>
|
||||
</c-resource-list>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fmt } from '@cortezaproject/corteza-js'
|
||||
import listHelpers from 'corteza-webapp-admin/src/mixins/listHelpers'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
listHelpers,
|
||||
],
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.connections',
|
||||
keyPrefix: 'federation',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
id: 'federation',
|
||||
|
||||
sorting: {
|
||||
sortBy: 'createdAt',
|
||||
sortDesc: true,
|
||||
},
|
||||
|
||||
fields: [
|
||||
{
|
||||
key: 'name',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
sortable: true,
|
||||
tdClass: 'text-info',
|
||||
},
|
||||
{
|
||||
key: 'location',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'ownership',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'createdBy',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
sortable: true,
|
||||
formatter: v => v ? fmt.fullDateTime(v) : v,
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
class: 'text-right',
|
||||
},
|
||||
].map(c => ({
|
||||
...c,
|
||||
// Generate column label translation key
|
||||
label: c.label || this.$t(`columns.${c.key}`),
|
||||
})),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
items () {
|
||||
const set = [
|
||||
{ federationID: '1', name: 'ACME France', url: 'https://corteza.acme.fr', location: 'France', ownership: 'ACME SARL', createdBy: 'John Doe', createdAt: new Date() },
|
||||
]
|
||||
|
||||
const filter = {
|
||||
count: set.length,
|
||||
limit: 10,
|
||||
}
|
||||
|
||||
return this.procListResults(new Promise(resolve => setTimeout(resolve({ filter, set })), 200), false)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
49
client/web/admin/src/components/Federation/C3.js
Normal file
49
client/web/admin/src/components/Federation/C3.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { default as component } from './CFederationEditorInfo.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
node: {
|
||||
nodeID: '',
|
||||
name: 'Name',
|
||||
baseURL: 'https://example.com/federation',
|
||||
contact: 'email@example.com',
|
||||
},
|
||||
processing: false,
|
||||
success: false,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Editor info',
|
||||
group: ['Federation'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('Enable delete', {
|
||||
value (p) { return p.node.nodeID.length > 0 },
|
||||
update (p, val) { p.node.nodeID = val ? '123456789' : '' },
|
||||
}),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
node: {
|
||||
...props.node,
|
||||
name: '',
|
||||
baseURL: '',
|
||||
contact: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', node)"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="node.name"
|
||||
:state="nameState"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('url')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="node.baseURL"
|
||||
placeholder="https://example.com/federation"
|
||||
type="url"
|
||||
:state="urlState"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('email')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="node.contact"
|
||||
placeholder="email@example.com"
|
||||
type="email"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="node.status"
|
||||
:label="$t('status')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ node.status }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:resource="node"
|
||||
/>
|
||||
|
||||
<!--
|
||||
include hidden input to enable
|
||||
trigger submit event w/ ENTER
|
||||
-->
|
||||
<input
|
||||
type="submit"
|
||||
class="d-none"
|
||||
:disabled="saveDisabled"
|
||||
>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="node && node.nodeID"
|
||||
variant="danger"
|
||||
size="md"
|
||||
:text="getDeleteStatus"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', node)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
name: 'CFederationEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'federation.nodes',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return !this.node.nodeID || this.node.nodeID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : this.node.canManageNode
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.nameState, this.urlState].includes(false)
|
||||
},
|
||||
|
||||
nameState () {
|
||||
const { name } = this.node
|
||||
return name ? null : false
|
||||
},
|
||||
|
||||
urlState () {
|
||||
const { baseURL = '' } = this.node
|
||||
return /https?:\/\/*.*\/federation/gm.test(baseURL) ? null : false
|
||||
},
|
||||
|
||||
getDeleteStatus () {
|
||||
return this.node.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-inline-block"
|
||||
>
|
||||
<b-button
|
||||
data-test-id="button-clone"
|
||||
variant="light"
|
||||
class="mr-2"
|
||||
@click="showModal = true"
|
||||
>
|
||||
{{ $t('ui.clone.label') }}
|
||||
</b-button>
|
||||
|
||||
<b-modal
|
||||
v-model="showModal"
|
||||
data-test-id="modal-clone-permission"
|
||||
ok-variant="primary"
|
||||
cancel-variant="light"
|
||||
centered
|
||||
:title="$t('ui.clone.title')"
|
||||
:ok-title="$t('ui.clone.clone')"
|
||||
:ok-disabled="!selectedRoles.length || processingSubmit"
|
||||
@ok="clonePermissions"
|
||||
>
|
||||
<b-form-group
|
||||
:description="$t('ui.clone.description')"
|
||||
class="mb-0"
|
||||
>
|
||||
<c-input-role
|
||||
v-model="selectedRoles"
|
||||
data-test-id="select-role-list"
|
||||
:selectable="r => !selectedRoles.some(rr => rr.roleID === r.roleID)"
|
||||
:placeholder="$t('ui.clone.pick-role')"
|
||||
multiple
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputRole } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'permissions',
|
||||
},
|
||||
|
||||
components: {
|
||||
CInputRole,
|
||||
},
|
||||
|
||||
props: {
|
||||
roleId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
showModal: false,
|
||||
|
||||
selectedRoles: [],
|
||||
|
||||
processingSubmit: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clonePermissions () {
|
||||
this.processingSubmit = true
|
||||
|
||||
const cloneToRoleID = this.selectedRoles.map(({ roleID }) => roleID)
|
||||
|
||||
this.$SystemAPI.roleCloneRules({ roleID: this.roleId, cloneToRoleID })
|
||||
.then(() => {
|
||||
this.selectedRoles = []
|
||||
this.toastSuccess(this.$t('notification:permissions.clone.success'))
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:permissions.clone.error')))
|
||||
.finally(() => {
|
||||
this.processingSubmit = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
526
client/web/admin/src/components/Permissions/CPermissionList.vue
Normal file
526
client/web/admin/src/components/Permissions/CPermissionList.vue
Normal file
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<div class="h-100">
|
||||
<b-card
|
||||
header-class="p-0"
|
||||
body-class="overflow-auto p-0"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow h-100"
|
||||
>
|
||||
<template
|
||||
v-if="loaded && canGrant"
|
||||
#header
|
||||
>
|
||||
<b-row
|
||||
no-gutters
|
||||
align-v="stretch"
|
||||
class="border-bottom"
|
||||
>
|
||||
<b-col
|
||||
cols="4"
|
||||
class="text-left p-3"
|
||||
>
|
||||
<small>
|
||||
{{ $t('ui.click-on-cell-to-allow') }}
|
||||
</small>
|
||||
</b-col>
|
||||
<b-col
|
||||
v-for="role in roles"
|
||||
:key="role.ID"
|
||||
data-test-id="button-hide-role"
|
||||
class="d-flex flex-column align-items-center justify-content-center pointer hide-role border-left p-3 overflow-hidden"
|
||||
@click="onHideRole(role)"
|
||||
>
|
||||
<label
|
||||
v-for="(n, index) in role.name"
|
||||
:key="index"
|
||||
:title="n"
|
||||
class="pointer text-center text-primary text-break mb-1"
|
||||
>
|
||||
{{ n }}
|
||||
</label>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'plus']"
|
||||
class="text-light rotate"
|
||||
/>
|
||||
</b-col>
|
||||
<b-col
|
||||
v-if="roles.length < 8"
|
||||
v-b-modal.add
|
||||
data-test-id="button-add-role"
|
||||
class="d-flex flex-column align-items-center justify-content-center border-left p-3 overflow-hidden"
|
||||
>
|
||||
<label
|
||||
class="pointer text-center text-primary text-break mb-1"
|
||||
>
|
||||
{{ $t('ui.add.label') }}
|
||||
</label>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'plus']"
|
||||
class="text-success"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="!loaded || !canGrant"
|
||||
class="d-flex align-items-center justify-content-center h-100 pb-4"
|
||||
>
|
||||
<div
|
||||
v-if="!loaded"
|
||||
>
|
||||
<b-spinner
|
||||
class="align-middle m-5"
|
||||
/>
|
||||
<div>
|
||||
{{ $t('ui.loading') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!canGrant"
|
||||
class="text-danger"
|
||||
>
|
||||
{{ $t('ui.not-allowed') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(type, i) in sortedPermissions"
|
||||
:key="type"
|
||||
>
|
||||
<b-row
|
||||
class="bg-light border-bottom text-primary sticky-top"
|
||||
align-v="stretch"
|
||||
no-gutters
|
||||
>
|
||||
<b-col
|
||||
cols="4"
|
||||
align-self="center"
|
||||
class="p-3 text-left"
|
||||
>
|
||||
<span class="h-100 h6 mb-0">
|
||||
{{ getTranslation(type) }}
|
||||
</span>
|
||||
</b-col>
|
||||
<b-col
|
||||
v-for="role in roles"
|
||||
:key="role.ID"
|
||||
class="d-flex align-items-center justify-content-center overflow-hidden p-3 text-center border-left not-allowed"
|
||||
>
|
||||
<p
|
||||
v-if="i === 0"
|
||||
:title="$t(`ui.${role.mode === 'edit' ? 'edit' : 'evaluate'}.title`)"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t(`ui.${role.mode === 'edit' ? 'edit' : 'evaluate'}.title`) }}
|
||||
</p>
|
||||
</b-col>
|
||||
<b-col
|
||||
class="p-3 border-left not-allowed"
|
||||
/>
|
||||
</b-row>
|
||||
<b-row
|
||||
v-for="operation in permissions[type].ops"
|
||||
:key="operation"
|
||||
:data-test-id="`permission-${operation}`"
|
||||
no-gutters
|
||||
>
|
||||
<b-col
|
||||
class="border-bottom text-left p-3"
|
||||
cols="4"
|
||||
>
|
||||
<span
|
||||
:title="getTranslation(type, operation)"
|
||||
>
|
||||
{{ getTranslation(type, operation) }}
|
||||
</span>
|
||||
</b-col>
|
||||
<b-col
|
||||
v-for="role in roles"
|
||||
:key="role.ID"
|
||||
v-b-tooltip.noninteractive.hover="{ title: getRuleTooltip(checkRule(role.ID, permissions[type].any, operation, 'unknown-context'), !!role.userID), boundary: 'body' }"
|
||||
class="d-flex align-items-center justify-content-center border-bottom border-left p-3 pointer active-cell h5 mb-0"
|
||||
:class="{
|
||||
'not-allowed bg-extra-light': role.mode === 'eval',
|
||||
'bg-warning': checkChange(role.ID, permissions[type].any, operation)
|
||||
}"
|
||||
@click="role.mode === 'edit' ? ruleChange($event, role.ID, permissions[type].any, operation) : undefined"
|
||||
>
|
||||
<font-awesome-icon
|
||||
v-if="checkRule(role.ID, permissions[type].any, operation, 'unknown-context')"
|
||||
data-test-id="permission-unknown"
|
||||
:icon="['fas', 'question']"
|
||||
class="text-secondary"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-else-if="checkRule(role.ID, permissions[type].any, operation, 'allow')"
|
||||
data-test-id="permission-allowed"
|
||||
:icon="['fas', 'check']"
|
||||
class="text-success"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-else
|
||||
data-test-id="permission-denied"
|
||||
:icon="['fas', 'times']"
|
||||
class="text-danger"
|
||||
/>
|
||||
</b-col>
|
||||
<b-col
|
||||
v-if="roles.length < 8"
|
||||
class="border-bottom border-left p-3 not-allowed bg-extra-light"
|
||||
/>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="loaded && canGrant"
|
||||
#footer
|
||||
>
|
||||
<c-button-submit
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('ui.save')"
|
||||
class="ml-auto"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
|
||||
<b-modal
|
||||
id="add"
|
||||
:title="$t('ui.edit-or-eval')"
|
||||
centered
|
||||
ok-only
|
||||
:ok-title="$t('ui.add.save')"
|
||||
:ok-disabled="!addEnabled"
|
||||
@ok="onAdd"
|
||||
>
|
||||
<b-form-group>
|
||||
<b-form-radio-group
|
||||
v-model="add.mode"
|
||||
data-test-id="button-mode"
|
||||
:options="modeOptions"
|
||||
buttons
|
||||
button-variant="outline-primary"
|
||||
class="mode rounded w-100"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<p>
|
||||
{{ addModeDescription }}
|
||||
</p>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('ui.add.role.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<c-input-role
|
||||
v-model="add.roleID"
|
||||
:data-test-id="`select-${add.mode}-roles`"
|
||||
:placeholder="$t('ui.add.role.placeholder')"
|
||||
:visible="isRoleVisible"
|
||||
:multiple="add.mode === 'eval'"
|
||||
:disabled="add.mode === 'eval' && !!add.userID"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="add.mode === 'eval'"
|
||||
:label="$t('ui.add.user.label')"
|
||||
label-class="text-primary"
|
||||
class="mt-3 mb-0"
|
||||
>
|
||||
<c-input-select
|
||||
v-model="add.userID"
|
||||
:data-test-id="`select-${add.mode}-users`"
|
||||
:disabled="!!add.roleID.length"
|
||||
:options="userOptions"
|
||||
:get-option-label="getUserLabel"
|
||||
:placeholder="$t('ui.add.user.placeholder')"
|
||||
:filterable="false"
|
||||
@search="searchUsers"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputRole } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'permissions',
|
||||
},
|
||||
|
||||
components: {
|
||||
CInputRole,
|
||||
},
|
||||
|
||||
props: {
|
||||
roles: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
permissions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
rolePermissions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
canGrant: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
loaded: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
component: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
add: {
|
||||
// Either edit or eval
|
||||
mode: 'edit',
|
||||
roleID: [],
|
||||
userID: undefined,
|
||||
},
|
||||
|
||||
modeOptions: [
|
||||
{ text: 'Edit', value: 'edit' },
|
||||
{ text: 'Evaluate', value: 'eval' },
|
||||
],
|
||||
|
||||
userOptions: [],
|
||||
|
||||
evaluatedPermissions: undefined,
|
||||
|
||||
newRole: null,
|
||||
permissionChanges: [],
|
||||
|
||||
fetchedUsers: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
sortedPermissions () {
|
||||
return Object.keys(this.permissions).sort()
|
||||
},
|
||||
|
||||
addModeDescription () {
|
||||
return this.add.mode === 'edit' ? this.$t('ui.add.edit.description') : this.$t('ui.add.evaluate.description')
|
||||
},
|
||||
|
||||
addEnabled () {
|
||||
const { mode, roleID = [], userID } = this.add
|
||||
|
||||
if (mode === 'edit') {
|
||||
return Array.isArray(roleID) ? roleID.length : roleID
|
||||
} else if (mode === 'eval') {
|
||||
return (roleID && roleID.length) || userID
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'add.mode': {
|
||||
handler (mode) {
|
||||
this.add.roleID = mode === 'eval' ? [] : undefined
|
||||
this.add.userID = undefined
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.searchUsers('', () => {})
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.setDefaultValues()
|
||||
},
|
||||
|
||||
methods: {
|
||||
checkRule (ID, res, op, access) {
|
||||
const key = `${op}@${res}`
|
||||
return (this.rolePermissions.find(r => r.ID === ID) || { rules: {} }).rules[key] === access
|
||||
},
|
||||
|
||||
checkChange (ID, res, op) {
|
||||
const key = `${op}@${res}`
|
||||
const current = (this.rolePermissions.find(r => r.ID === ID) || { rules: {} }).rules[key]
|
||||
const initial = (this.permissionChanges.find(r => r.ID === ID) || { rules: {} }).rules[key]
|
||||
|
||||
if (initial) {
|
||||
return current !== initial
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
ruleChange (event, ID, res, op) {
|
||||
const key = `${op}@${res}`
|
||||
let access = (this.rolePermissions.find(r => r.ID === ID) || { rules: {} }).rules[key]
|
||||
|
||||
// Keep track of permission changes, record initial value before it changes
|
||||
if (!(this.permissionChanges.find(r => r.ID === ID) || { rules: {} }).rules[key]) {
|
||||
this.permissionChanges.push({ ID, rules: { } })
|
||||
|
||||
if (!access) {
|
||||
access = 'inherit'
|
||||
}
|
||||
this.$set(this.permissionChanges.find(r => r.ID === ID).rules, key, access)
|
||||
}
|
||||
|
||||
if (access === 'allow') {
|
||||
access = 'inherit'
|
||||
} else {
|
||||
access = 'allow'
|
||||
}
|
||||
|
||||
this.$set(this.rolePermissions.find(r => r.ID === ID).rules, key, access)
|
||||
},
|
||||
|
||||
searchUsers (query = '', loading) {
|
||||
loading(true)
|
||||
|
||||
this.$SystemAPI.userList({ query, limit: 15 })
|
||||
.then(({ set }) => {
|
||||
this.userOptions = set.reduce((acc, { userID, name, username, email }) => {
|
||||
if (!this.fetchedUsers[userID]) {
|
||||
this.fetchedUsers[userID] = name || username || email || `<@${userID}>`
|
||||
}
|
||||
|
||||
acc.push(userID)
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
.finally(() => {
|
||||
loading(false)
|
||||
})
|
||||
},
|
||||
|
||||
isRoleVisible ({ isBypass }) {
|
||||
return this.add.mode === 'edit' || !isBypass
|
||||
},
|
||||
|
||||
getUserLabel (userID) {
|
||||
return this.fetchedUsers[userID]
|
||||
},
|
||||
|
||||
getTranslation (resource, operation = '') {
|
||||
resource = _.kebabCase(resource.split(':')[3]) || 'component'
|
||||
|
||||
if (operation) {
|
||||
return this.$t(`resources.${this.component}.${resource}.operations.${operation}.title`)
|
||||
} else {
|
||||
return this.$t(`resources.${this.component}.${resource}.label`)
|
||||
}
|
||||
},
|
||||
|
||||
getRuleTooltip (isUnknown = false, isUser) {
|
||||
if (!isUnknown) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.$t(`ui.tooltip.unknown-context.${isUser ? 'user' : 'role'}`)
|
||||
},
|
||||
|
||||
onSubmit () {
|
||||
this.$emit('submit', this.rolePermissions)
|
||||
this.permissionChanges = []
|
||||
},
|
||||
|
||||
onAdd () {
|
||||
let { userID } = this.add
|
||||
|
||||
if (userID) {
|
||||
userID = { userID: this.add.userID, name: this.fetchedUsers[this.add.userID] }
|
||||
}
|
||||
|
||||
this.$emit('add', { ...this.add, userID })
|
||||
|
||||
this.add = {
|
||||
mode: 'edit',
|
||||
roleID: [],
|
||||
userID: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
onHideRole (role) {
|
||||
this.$emit('hide', role)
|
||||
},
|
||||
|
||||
setDefaultValues () {
|
||||
this.add = {}
|
||||
this.modeOptions = []
|
||||
this.userOptions = []
|
||||
this.evaluatedPermissions = undefined
|
||||
this.newRole = null
|
||||
this.permissionChanges = []
|
||||
this.fetchedUsers = {}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.active-cell:hover {
|
||||
background-color: var(--gray-200);
|
||||
}
|
||||
.rotate {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.hide-role:hover {
|
||||
.rotate {
|
||||
color: var(--dark) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.mode {
|
||||
.btn {
|
||||
background-color: var(--light);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn:nth-child(2), .btn:nth-child(3) {
|
||||
margin-left: 0.2rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
client/web/admin/src/components/Queues/C3.js
Normal file
39
client/web/admin/src/components/Queues/C3.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { default as component } from './CQueueEditorInfo.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
queue: {
|
||||
queueID: '',
|
||||
queue: 'Name',
|
||||
meta: {
|
||||
poll_delay: '1h',
|
||||
dispatch_events: 'test',
|
||||
},
|
||||
consumer: null,
|
||||
},
|
||||
processing: false,
|
||||
success: false,
|
||||
canCreate: true,
|
||||
consumers: [
|
||||
{ value: 'dummy', text: 'Dummy' },
|
||||
],
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Editor info',
|
||||
group: ['Queues'],
|
||||
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('CanCreate', 'canCreate'),
|
||||
checkbox('Enable delete', {
|
||||
value (p) { return p.queue.queueID.length > 0 },
|
||||
update (p, val) { p.queue.queueID = val ? '123456789' : '' },
|
||||
}),
|
||||
],
|
||||
}
|
||||
216
client/web/admin/src/components/Queues/CQueueEditorInfo.vue
Normal file
216
client/web/admin/src/components/Queues/CQueueEditorInfo.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-queue-edit"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t("title") }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form @submit="$emit('submit', queue)">
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="queue.queue"
|
||||
data-test-id="input-name"
|
||||
:state="handleState"
|
||||
/>
|
||||
<b-form-invalid-feedback
|
||||
:state="handleState"
|
||||
data-test-id="feedback-invalid-name"
|
||||
>
|
||||
{{ $t('invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('consumer')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="queue.consumer"
|
||||
data-test-id="input-consumer"
|
||||
:options="consumers"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('poll_delay')"
|
||||
:description="metaPollDelayDescription()"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="(queue.meta || {}).poll_delay"
|
||||
data-test-id="input-polling"
|
||||
:state="durationState"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="isMetaDispatchEvents"
|
||||
:label="$t('dispatch_events')"
|
||||
:description="$t('dispatch_events_desc')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="queue.meta.dispatch_events"
|
||||
name="checkbox-1"
|
||||
>
|
||||
{{ $t("dispatch_events") }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:resource="queue"
|
||||
/>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="queue && queue.queueID && queue.canDeleteQueue"
|
||||
:data-test-id="deleteButtonStatusCypressId"
|
||||
:text="getDeleteStatus"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', queue)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
import { handle } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export default {
|
||||
name: 'CQueueEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.queues',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
props: {
|
||||
consumers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
queue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return !this.queue.queueID || this.queue.queueID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : true // this.queue.canUpdateQueue
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.durationState, this.handleState].includes(false)
|
||||
},
|
||||
|
||||
durationState () {
|
||||
const pd = (this.queue.meta || {}).poll_delay || ''
|
||||
const m = pd.match(/^((\d+h)?(\d+m)?(\d+s)?)|(\s)$/g)
|
||||
|
||||
if (m.length && m[0] === pd) {
|
||||
return null
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
handleState () {
|
||||
const { queue = '' } = this.queue
|
||||
return queue ? handle.handleState(queue) : false
|
||||
},
|
||||
|
||||
isMetaPollDelay () {
|
||||
if (this.queue.queueID) {
|
||||
return ((this.queue.meta || {}).poll_delay || '') === ''
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
isMetaDispatchEvents () {
|
||||
return ((this.queue || {}).meta || {}).dispatch_events === null
|
||||
},
|
||||
|
||||
getDeleteStatus () {
|
||||
return this.queue.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
|
||||
deleteButtonStatusCypressId () {
|
||||
return `button-${this.getDeleteStatus.toLowerCase()}`
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
metaPollDelayDescription () {
|
||||
return (((this.queue || {}).meta || {}).poll_delay || null)
|
||||
? this.$t('poll_delay_set')
|
||||
: this.$t('poll_delay_empty')
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
1
client/web/admin/src/components/Role/C3.js
Normal file
1
client/web/admin/src/components/Role/C3.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CRoleEditorInfo } from './CRoleEditorInfo.c3'
|
||||
50
client/web/admin/src/components/Role/CRoleEditorInfo.c3.js
Normal file
50
client/web/admin/src/components/Role/CRoleEditorInfo.c3.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { default as component } from './CRoleEditorInfo.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
role: {
|
||||
roleID: '',
|
||||
name: 'Federation',
|
||||
handle: 'federation',
|
||||
},
|
||||
processing: false,
|
||||
success: false,
|
||||
canCreate: true,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Editor info',
|
||||
group: ['Role'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('CanCreate', 'canCreate'),
|
||||
checkbox('Enable delete and archive', {
|
||||
value (p) { return p.role.roleID.length > 0 },
|
||||
update (p, val) { p.role.roleID = val ? '123456789' : '' },
|
||||
}),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
role: {
|
||||
...props.role,
|
||||
name: '',
|
||||
handle: '',
|
||||
},
|
||||
canCreate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
325
client/web/admin/src/components/Role/CRoleEditorInfo.vue
Normal file
325
client/web/admin/src/components/Role/CRoleEditorInfo.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-role-info"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="role.name"
|
||||
data-test-id="input-name"
|
||||
:state="nameState"
|
||||
:disabled="!editable"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('handle')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="role.handle"
|
||||
data-test-id="input-handle"
|
||||
:state="handleState"
|
||||
:disabled="!editable"
|
||||
:placeholder="$t('placeholder-handle')"
|
||||
/>
|
||||
|
||||
<b-form-invalid-feedback :state="handleState">
|
||||
{{ $t('invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
v-if="role.meta"
|
||||
:label="$t('description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="role.meta.description"
|
||||
data-test-id="textarea-description"
|
||||
:disabled="!editable"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('context.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="isContextual"
|
||||
data-test-id="checkbox-is-contextual"
|
||||
switch
|
||||
:labels="checkboxLabel"
|
||||
:disabled="!editable"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row
|
||||
v-if="isContextual"
|
||||
class="my-3"
|
||||
>
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('context.expression-label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="role.meta.context.expr"
|
||||
data-test-id="input-expression"
|
||||
:disabled="!editable"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('context.resource-types-label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-checkbox
|
||||
v-for="(resourceType, i) in resourceTypeOptions"
|
||||
:key="i"
|
||||
v-model="role.meta.context.resourceTypes"
|
||||
:data-test-id="`checkbox-resource-type-${resourceType.text}`"
|
||||
:value="resourceType.value"
|
||||
:disabled="!editable"
|
||||
>
|
||||
{{ resourceType.text }}
|
||||
</b-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:resource="role"
|
||||
/>
|
||||
|
||||
<!--
|
||||
include hidden input to enable
|
||||
trigger submit event w/ ENTER
|
||||
-->
|
||||
<input
|
||||
type="submit"
|
||||
class="d-none"
|
||||
:disabled="saveDisabled"
|
||||
>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="!fresh && editable && role.canDeleteRole && !isDataPrivacyOfficer"
|
||||
:data-test-id="deletedButtonStatusCypressId"
|
||||
:text="getDeleteStatus"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
v-if="!fresh && editable && !isDataPrivacyOfficer"
|
||||
:data-test-id="archivedButtonStatusCypressId"
|
||||
:text="getArchiveStatus"
|
||||
variant="secondary"
|
||||
size="md"
|
||||
@confirmed="$emit('status')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="submit()"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { system, NoID } from '@cortezaproject/corteza-js'
|
||||
import { handle } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export default {
|
||||
name: 'CRoleEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.roles',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
props: {
|
||||
role: {
|
||||
type: system.Role,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isContext: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
checkboxLabel: {
|
||||
on: this.$t('general:label.general.yes'),
|
||||
off: this.$t('general:label.general.no'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isContextual: {
|
||||
get () {
|
||||
return this.isContext
|
||||
},
|
||||
|
||||
set (isContext) {
|
||||
this.role.meta.context.resourceTypes = []
|
||||
this.role.meta.context.expr = ''
|
||||
|
||||
this.$emit('update:is-context', isContext)
|
||||
},
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.nameState, this.handleState].includes(false)
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if role is not saved yet (without ID)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
fresh () {
|
||||
return !this.role.roleID || this.role.roleID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : !this.role.isSystem && this.role.canUpdateRole
|
||||
},
|
||||
|
||||
// At least 1 character
|
||||
nameState () {
|
||||
const { name } = this.role
|
||||
return name ? null : false
|
||||
},
|
||||
|
||||
// 2+ alpha-numeric + _
|
||||
handleState () {
|
||||
return handle.handleState(this.role.handle)
|
||||
},
|
||||
|
||||
getDeleteStatus () {
|
||||
return this.role.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
|
||||
getArchiveStatus () {
|
||||
return this.role.archivedAt ? this.$t('unarchive') : this.$t('archive')
|
||||
},
|
||||
|
||||
resourceTypeOptions () {
|
||||
return this.resourceTypes.map(value => ({
|
||||
// @todo use translation facility to generate resource type option labels
|
||||
text: value.replace('corteza::', ''),
|
||||
value,
|
||||
}))
|
||||
},
|
||||
|
||||
resourceTypes () {
|
||||
// @todo this should be fetched from the backend
|
||||
return [
|
||||
// 'corteza::system:application',
|
||||
'corteza::system:auth-client',
|
||||
'corteza::system:role',
|
||||
// 'corteza::system:template',
|
||||
'corteza::system:user',
|
||||
// 'corteza::compose:chart',
|
||||
// 'corteza::compose:module-field',
|
||||
'corteza::compose:module',
|
||||
'corteza::compose:namespace',
|
||||
'corteza::compose:page',
|
||||
'corteza::compose:record',
|
||||
'corteza::automation:workflow',
|
||||
// 'corteza::federation:exposed-module',
|
||||
// 'corteza::federation:node',
|
||||
// 'corteza::federation:shared-module',
|
||||
]
|
||||
},
|
||||
|
||||
deletedButtonStatusCypressId () {
|
||||
return `button-${this.getDeleteStatus.toLowerCase()}`
|
||||
},
|
||||
|
||||
archivedButtonStatusCypressId () {
|
||||
return `button-${this.getArchiveStatus.toLowerCase()}`
|
||||
},
|
||||
|
||||
isDataPrivacyOfficer () {
|
||||
return this.role.handle === 'data-privacy-officer'
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit () {
|
||||
if (!this.isContextual && this.role.isContext) {
|
||||
// if checkbox was unchecked on submit, purge meta before submit
|
||||
this.role.meta.context.resourceTypes = []
|
||||
this.role.meta.context.expr = ''
|
||||
}
|
||||
this.$emit('submit', this.role)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
64
client/web/admin/src/components/Role/CRoleEditorMembers.vue
Normal file
64
client/web/admin/src/components/Role/CRoleEditorMembers.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-role-edit-members"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form @submit.prevent="$emit('submit')">
|
||||
<c-member-picker
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CMemberPicker from 'corteza-webapp-admin/src/components/CMemberPicker'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.roles',
|
||||
keyPrefix: 'editor.members',
|
||||
},
|
||||
|
||||
components: {
|
||||
CMemberPicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-sens-lvl-info"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', sensitivityLevel)"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="sensitivityLevel.meta.name"
|
||||
data-test-id="input-name"
|
||||
required
|
||||
:state="nameState"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('handle.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="sensitivityLevel.handle"
|
||||
:placeholder="$t('handle.placeholder')"
|
||||
:state="handleState"
|
||||
/>
|
||||
|
||||
<b-form-invalid-feedback :state="handleState">
|
||||
{{ $t('handle.invalid-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('level', sensitivityLevel)"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="sensitivityLevel.level"
|
||||
number
|
||||
type="range"
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('description')"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="sensitivityLevel.meta.description"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<c-system-fields
|
||||
:resource="sensitivityLevel"
|
||||
/>
|
||||
<!--
|
||||
include hidden input to enable
|
||||
trigger submit event w/ ENTER
|
||||
-->
|
||||
<input
|
||||
type="submit"
|
||||
class="d-none"
|
||||
:disabled="saveDisabled"
|
||||
>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="sensitivityLevel && sensitivityLevel.sensitivityLevelID"
|
||||
:text="getDeleteStatus"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', sensitivityLevel)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
import { handle } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export default {
|
||||
name: 'CSensitivityLevelEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.sensitivityLevel',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
props: {
|
||||
sensitivityLevel: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return !this.sensitivityLevel.sensitivityLevelID || this.sensitivityLevel.sensitivityLevelID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : true
|
||||
},
|
||||
|
||||
nameState () {
|
||||
return this.sensitivityLevel.meta.name ? null : false
|
||||
},
|
||||
|
||||
handleState () {
|
||||
return handle.handleState(this.sensitivityLevel.handle)
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.nameState, this.handleState].includes(false)
|
||||
},
|
||||
|
||||
getDeleteStatus () {
|
||||
return this.sensitivityLevel.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
176
client/web/admin/src/components/Session/CSessionEditorInfo.vue
Normal file
176
client/web/admin/src/components/Session/CSessionEditorInfo.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<b-card
|
||||
class="shadow-sm"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap gap-1"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
v-if="session && session.sessionID"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
v-if="session.workflowID"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('workflowID')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ session.workflowID }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('status')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ session.status }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="session.error"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('error')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ session.error }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="session.resourceType"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('resourceType')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ session.resourceType }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="session.eventType"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('eventType')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ session.eventType }}
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="!session.completedAt"
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-button
|
||||
variant="danger"
|
||||
:disabled="processing"
|
||||
@click="$emit('cancel')"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:id="session.sessionID"
|
||||
:resource="session"
|
||||
>
|
||||
<template #custom-field>
|
||||
<b-col
|
||||
v-if="createdByUserName"
|
||||
cols="12"
|
||||
lg="4"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('createdByUserName')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-user-label :user="user" />
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</template>
|
||||
</c-system-fields>
|
||||
</b-form>
|
||||
|
||||
<template
|
||||
v-if="session.workflowID"
|
||||
#footer
|
||||
>
|
||||
<b-button
|
||||
variant="light"
|
||||
:to="{ name: 'automation.workflow.edit', params: { workflowID: session.workflowID } }"
|
||||
>
|
||||
{{ $t('openWorkflow') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CUserLabel } = components
|
||||
|
||||
export default {
|
||||
name: 'CSessionEditorInfo',
|
||||
|
||||
components: {
|
||||
CUserLabel,
|
||||
},
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'automation.sessions',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
props: {
|
||||
session: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
createdByUserName () {
|
||||
const { userID, name, username, email } = this.user
|
||||
return name || username || email || `<@${userID}>`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
3
client/web/admin/src/components/Settings/C3.js
Normal file
3
client/web/admin/src/components/Settings/C3.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as CComposeEditorBasic } from './Compose/CComposeEditorBasic.c3'
|
||||
export { default as CSystemEditorAuth } from './System/CSystemEditorAuth.c3'
|
||||
export { default as CSystemEmailServer } from './Email/CSystemEmailServer.c3'
|
||||
@@ -0,0 +1,45 @@
|
||||
import { default as component } from './CComposeEditorBasic.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
basic: {
|
||||
'compose.page.attachments.max-size': 22,
|
||||
'compose.record.attachments.max-size': 22,
|
||||
},
|
||||
processing: false,
|
||||
success: false,
|
||||
canManage: true,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Compose editor basic',
|
||||
group: ['Settings'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('CanManage', 'canManage'),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
basic: {
|
||||
...props.basic,
|
||||
'compose.page.attachments.max-size': '',
|
||||
'compose.record.attachments.max-size': '',
|
||||
},
|
||||
canCreate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', basic)"
|
||||
>
|
||||
<div class="pb-3">
|
||||
<h5>{{ $t('attachments.namespace') }}</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('attachments.max-size')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="basic['compose.namespace.attachments.max-size']"
|
||||
type="number"
|
||||
number
|
||||
placeholder="0"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('attachments.type.whitelist')"
|
||||
:description="$t('attachments.type.description')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-input v-model="namespaceAttachmentWhitelist" />
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<div class="pb-3">
|
||||
<h5>{{ $t('attachments.page') }}</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('attachments.max-size')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="basic['compose.page.attachments.max-size']"
|
||||
type="number"
|
||||
number
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('attachments.type.whitelist')"
|
||||
:description="$t('attachments.type.description')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-input v-model="pageAttachmentWhitelist" />
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<div class="pb-3">
|
||||
<h5>{{ $t('attachments.record') }}</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('attachments.max-size')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="basic['compose.record.attachments.max-size']"
|
||||
type="number"
|
||||
number
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('attachments.type.whitelist')"
|
||||
:description="$t('attachments.type.description')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-input v-model="recordAttachmentWhitelist" />
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5>{{ $t('attachments.icon') }}</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('attachments.max-size')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="basic['compose.icon.attachments.max-size']"
|
||||
type="number"
|
||||
number
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('attachments.type.whitelist')"
|
||||
:description="$t('attachments.type.description')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-input v-model="iconAttachmentWhitelist" />
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
v-if="canManage"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', basic)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'CComposeEditorBasic',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'compose.settings',
|
||||
keyPrefix: 'editor.basic',
|
||||
},
|
||||
|
||||
props: {
|
||||
basic: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canManage: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageAttachmentWhitelist: {
|
||||
get () {
|
||||
return (this.basic['compose.page.attachments.mimetypes'] || []).join(',')
|
||||
},
|
||||
|
||||
set (value) {
|
||||
this.basic['compose.page.attachments.mimetypes'] = this.convertToExternal(value)
|
||||
},
|
||||
},
|
||||
|
||||
recordAttachmentWhitelist: {
|
||||
get () {
|
||||
return (this.basic['compose.record.attachments.mimetypes'] || []).join(',')
|
||||
},
|
||||
|
||||
set (value) {
|
||||
this.basic['compose.record.attachments.mimetypes'] = this.convertToExternal(value)
|
||||
},
|
||||
},
|
||||
|
||||
iconAttachmentWhitelist: {
|
||||
get () {
|
||||
return (this.basic['compose.icon.attachments.mimetypes'] || []).join(',')
|
||||
},
|
||||
|
||||
set (value) {
|
||||
this.basic['compose.icon.attachments.mimetypes'] = this.convertToExternal(value)
|
||||
},
|
||||
},
|
||||
|
||||
namespaceAttachmentWhitelist: {
|
||||
get () {
|
||||
return (this.basic['compose.namespace.attachments.mimetypes'] || []).join(',')
|
||||
},
|
||||
|
||||
set (value) {
|
||||
this.basic['compose.namespace.attachments.mimetypes'] = this.convertToExternal(value)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
convertToExternal (value) {
|
||||
return (value || '').split(',').map(v => {
|
||||
return v.replace(/ /g, '')
|
||||
}).filter(v => {
|
||||
return v.match(/^[-\w.]+\/[-\w/+.]+$/g) !== null
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', settings)"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('sidebar.title')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="sidebar.hideNamespaceList"
|
||||
>
|
||||
{{ $t('sidebar.hide-namespace-list') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="sidebar.hideNamespaceListLink"
|
||||
>
|
||||
{{ $t('sidebar.hide-namespace-list-link') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('record-toolbar.title')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="recordToolbar.hideSubmit"
|
||||
>
|
||||
{{ $t('record-toolbar.hide-submit') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="recordToolbar.hideDelete"
|
||||
>
|
||||
{{ $t('record-toolbar.hide-delete') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="recordToolbar.hideEdit"
|
||||
>
|
||||
{{ $t('record-toolbar.hide-edit') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="recordToolbar.hideNew"
|
||||
>
|
||||
{{ $t('record-toolbar.hide-new') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="recordToolbar.hideClone"
|
||||
>
|
||||
{{ $t('record-toolbar.hide-clone') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="recordToolbar.hideBack"
|
||||
>
|
||||
{{ $t('record-toolbar.hide-back') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
v-if="canManage"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'CComposeEditorUI',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'compose.settings',
|
||||
keyPrefix: 'editor.ui',
|
||||
},
|
||||
|
||||
props: {
|
||||
settings: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canManage: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
sidebar: {},
|
||||
recordToolbar: {},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
settings: {
|
||||
immediate: true,
|
||||
handler (settings) {
|
||||
this.sidebar = settings['compose.ui.sidebar'] || {}
|
||||
this.recordToolbar = settings['compose.ui.record-toolbar'] || {}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit () {
|
||||
this.$emit('submit', {
|
||||
'compose.ui.sidebar': this.sidebar,
|
||||
'compose.ui.record-toolbar': this.recordToolbar,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { default as component } from './CSystemEmailServer.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
value: {},
|
||||
processing: false,
|
||||
success: false,
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Email server',
|
||||
group: ['Settings'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('Disabled', 'disabled'),
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('host.label')"
|
||||
:description="$t('host.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-input
|
||||
v-model="server.host"
|
||||
data-test-id="input-server"
|
||||
:disabled="disabled"
|
||||
placeholder="host.domain.tld"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
|
||||
<b-input-group-append is-text>
|
||||
:
|
||||
</b-input-group-append>
|
||||
|
||||
<b-input-group-append>
|
||||
<b-input
|
||||
v-model="server.port"
|
||||
data-test-id="input-server-port"
|
||||
type="number"
|
||||
:disabled="disabled"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('user.label')"
|
||||
:description="$t('user.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="server.user"
|
||||
data-test-id="input-user"
|
||||
:disabled="disabled"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('password.label')"
|
||||
:description="$t('password.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="server.pass"
|
||||
data-test-id="input-password"
|
||||
type="password"
|
||||
:disabled="disabled"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('from.label')"
|
||||
:description="$t('from.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="server.from"
|
||||
data-test-id="input-sender-address"
|
||||
type="email"
|
||||
:disabled="disabled"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('tlsServerName.label')"
|
||||
:description="$t('tlsServerName.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input
|
||||
v-model="server.tlsServerName"
|
||||
data-test-id="input-tls-server-name"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:description="$t('tlsInsecure.description')"
|
||||
class="mt-lg-3"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="server.tlsInsecure"
|
||||
data-test-id="checkbox-allow-invalid-certificates"
|
||||
:disabled="disabled"
|
||||
class="mt-lg-4 mb-2"
|
||||
>
|
||||
{{ $t('tlsInsecure.label') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
data-test-id="button-smtp"
|
||||
:disabled="disabled"
|
||||
:processing="processingSMTPTest"
|
||||
:success="successSMTPTest"
|
||||
:text="$t('testSmtpConfigs.button')"
|
||||
variant="light"
|
||||
@submit="smtpConnectionCheck()"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="disabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="submit()"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'CComposeEditorBasic',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.email',
|
||||
keyPrefix: 'editor.server',
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
processingSMTPTest: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
successSMTPTest: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
server: Vue.util.extend({
|
||||
host: '',
|
||||
port: 25,
|
||||
user: '',
|
||||
pass: '',
|
||||
from: '',
|
||||
tlsInsecure: false,
|
||||
tlsServerName: '',
|
||||
}, this.value),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit () {
|
||||
this.$emit('submit', this.server)
|
||||
},
|
||||
|
||||
smtpConnectionCheck () {
|
||||
this.$emit('smtpConnectionCheck', this.server)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
const props = {
|
||||
// OIDC
|
||||
title: 'Title',
|
||||
value: {
|
||||
enabled: true,
|
||||
handle: 'Handle',
|
||||
issuer: 'Mara',
|
||||
key: 'key',
|
||||
secret: 'shhhhh',
|
||||
cert: 'cert himself',
|
||||
idp: {
|
||||
url: 'https://kati.com',
|
||||
'ident-name': 'kati',
|
||||
'ident-handle': 'kati',
|
||||
'ident-identifier': 'kati',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const controls = [
|
||||
{
|
||||
label: 'Title',
|
||||
type: 'b-form-input',
|
||||
value: props.title,
|
||||
handle: (props, value) => {
|
||||
props.title = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Enabled',
|
||||
type: 'b-form-checkbox',
|
||||
value: props.value.enabled,
|
||||
handle: (props, value) => {
|
||||
props.enabled = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Handle',
|
||||
type: 'b-form-input',
|
||||
value: props.value.handle,
|
||||
handle: (props, value) => {
|
||||
props.handle = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Issuer',
|
||||
type: 'b-form-input',
|
||||
value: props.value.issuer,
|
||||
handle: (props, value) => {
|
||||
props.issuer = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Key',
|
||||
type: 'b-form-input',
|
||||
value: props.value.key,
|
||||
handle: (props, value) => {
|
||||
props.key = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Secret',
|
||||
type: 'b-form-input',
|
||||
value: props.value.secret,
|
||||
handle: (props, value) => {
|
||||
props.secret = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Cert',
|
||||
type: 'b-form-input',
|
||||
value: props.value.cert,
|
||||
handle: (props, value) => {
|
||||
props.cert = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'url',
|
||||
type: 'b-form-input',
|
||||
value: props.value.idp.url,
|
||||
handle: (props, value) => {
|
||||
props.value.idp.url = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
type: 'b-form-input',
|
||||
value: props.value.idp['ident-name'],
|
||||
handle: (props, value) => {
|
||||
props.value.idp['ident-name'] = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Handle',
|
||||
type: 'b-form-input',
|
||||
value: props.value.idp['ident-handle'],
|
||||
handle: (props, value) => {
|
||||
props.value.idp['ident-handle'] = value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Identifier',
|
||||
type: 'b-form-input',
|
||||
value: props.value.idp['ident-identifier'],
|
||||
handle: (props, value) => {
|
||||
props.value.idp['ident-handle'] = value
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
label: 'full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'empty form',
|
||||
props: {
|
||||
title: 'null',
|
||||
value: {
|
||||
enabled: true,
|
||||
handle: 'null',
|
||||
issuer: 'null',
|
||||
key: 'null',
|
||||
secret: 'null',
|
||||
cert: 'null',
|
||||
idp: {
|
||||
url: 'null',
|
||||
'ident-name': 'null',
|
||||
'ident-handle': 'null',
|
||||
'ident-identifier': 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default {
|
||||
props,
|
||||
controls,
|
||||
scenarios,
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="value.enabled"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
>
|
||||
{{ $t('enabled') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('handle')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model.trim="value.handle"
|
||||
:formatter="alphanum"
|
||||
:disabled="!fresh"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('issuer')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model.trim="value.issuer"
|
||||
placeholder="https://issuer.tld"
|
||||
/>
|
||||
</b-input-group>
|
||||
<b-form-text
|
||||
v-html="$t('issuerHint')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('clientKey')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model.trim="value.key" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('clientSecret')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model.trim="value.secret" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('scope')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model.trim="value.scope"
|
||||
:placeholder="$t('scopePlaceholder')"
|
||||
/>
|
||||
</b-input-group>
|
||||
<b-form-text
|
||||
v-html="$t('scopeHint')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<security
|
||||
v-model="value.security"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Security from './ExternalSecurity'
|
||||
|
||||
export default {
|
||||
name: 'OIDCExternalAuthProvider',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.settings',
|
||||
keyPrefix: 'editor.external.oidc',
|
||||
},
|
||||
|
||||
components: {
|
||||
Security,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
const {
|
||||
enabled,
|
||||
key,
|
||||
secret,
|
||||
scope,
|
||||
} = this.value
|
||||
|
||||
return {
|
||||
enabled,
|
||||
key,
|
||||
secret,
|
||||
scope,
|
||||
|
||||
permittedRoles: [],
|
||||
prohibitedRoles: [],
|
||||
forcedRoles: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return Object.prototype.hasOwnProperty.call(this.value, 'fresh') && this.value.fresh
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
alphanum (v) {
|
||||
return v.replace(/[^a-zA-Z0-9\-_]+/, '')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="value.enabled"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
>
|
||||
{{ $t('enabled') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
:description="$t('desc.name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model.trim="value.name" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5>
|
||||
{{ $t('certificate') }}
|
||||
</h5>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('cert.public')"
|
||||
:description="$t('desc.cert.public')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-textarea
|
||||
v-model.trim="value.cert"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
:label="$t('cert.private')"
|
||||
:description="$t('desc.cert.private')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-textarea v-model.trim="value.key" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5>
|
||||
{{ $t('requests.title') }}
|
||||
</h5>
|
||||
|
||||
<b-form-group
|
||||
:description="$t('desc.requests.sign-requests')"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="value['sign-requests']"
|
||||
>
|
||||
{{ $t('requests.sign-requests') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('requests.sign-method')"
|
||||
:description="$t('desc.requests.sign-method')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-select
|
||||
v-model.trim="value['sign-method']"
|
||||
:options="signMethods"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
value=""
|
||||
disabled
|
||||
>
|
||||
{{ $t('admin:general.label.selectOption') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('requests.binding')"
|
||||
:description="$t('desc.requests.binding')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-select
|
||||
v-model.trim="value['binding']"
|
||||
:options="httpBindings"
|
||||
>
|
||||
<template #first>
|
||||
<b-form-select-option
|
||||
value=""
|
||||
disabled
|
||||
>
|
||||
{{ $t('admin:general.label.selectOption') }}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5>
|
||||
{{ $t('idp.title') }}
|
||||
</h5>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('idp.url')"
|
||||
:description="$t('desc.idp.url')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model.trim="value.idp.url" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('idp.ident-name')"
|
||||
:description="$t('desc.idp.ident-name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model.trim="value.idp['ident-name']" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('idp.ident-handle')"
|
||||
:description="$t('desc.idp.ident-handle')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model.trim="value.idp['ident-handle']" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('idp.ident-identifier')"
|
||||
:description="$t('desc.idp.ident-identifier')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model.trim="value.idp['ident-identifier']" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<security
|
||||
v-model="value.security"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Security from './ExternalSecurity'
|
||||
|
||||
export default {
|
||||
name: 'SamlExternalAuthProvider',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.settings',
|
||||
keyPrefix: 'editor.external.saml',
|
||||
},
|
||||
|
||||
components: {
|
||||
Security,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
signMethods: [
|
||||
{ value: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', text: 'SHA1' },
|
||||
{ value: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', text: 'SHA256' },
|
||||
{ value: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512', text: 'SHA512' },
|
||||
],
|
||||
httpBindings: [
|
||||
{ value: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', text: this.$t('requests.binding-post') },
|
||||
{ value: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', text: this.$t('requests.binding-redirect') },
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div>
|
||||
<hr>
|
||||
|
||||
<h5>
|
||||
{{ $t('title') }}
|
||||
</h5>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('permitted-roles.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<c-role-picker
|
||||
v-model="value.permittedRoles"
|
||||
class="mb-3"
|
||||
>
|
||||
<template #description>
|
||||
{{ $t('permitted-roles.description') }}
|
||||
</template>
|
||||
</c-role-picker>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('prohibited-roles.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<c-role-picker
|
||||
v-model="value.prohibitedRoles"
|
||||
class="mb-3"
|
||||
>
|
||||
<template #description>
|
||||
{{ $t('prohibited-roles.description') }}
|
||||
</template>
|
||||
</c-role-picker>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('forced-roles.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<c-role-picker
|
||||
v-model="value.forcedRoles"
|
||||
class="mb-3"
|
||||
>
|
||||
<template #description>
|
||||
{{ $t('forced-roles.description') }}
|
||||
</template>
|
||||
</c-role-picker>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CRolePicker from 'corteza-webapp-admin/src/components/CRolePicker'
|
||||
export default {
|
||||
name: 'StandardExternalAuthProvider',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.settings',
|
||||
keyPrefix: 'editor.external.security',
|
||||
},
|
||||
|
||||
components: {
|
||||
CRolePicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="value.enabled"
|
||||
>
|
||||
{{ $t('enabled') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('clientKey')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model.trim="value.key"
|
||||
:required="value.enabled"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('clientSecret')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model.trim="value.secret"
|
||||
:required="value.enabled"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<security
|
||||
v-model="value.security"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Security from './ExternalSecurity'
|
||||
export default {
|
||||
name: 'StandardExternalAuthProvider',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.settings',
|
||||
keyPrefix: 'editor.external.standard',
|
||||
},
|
||||
|
||||
components: {
|
||||
Security,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
// providerUsage provides a temporary checkbox implementation until something
|
||||
// more proper is done
|
||||
//
|
||||
// When checked (true), we consider it as wanting to use the API
|
||||
providerUsage: {
|
||||
get () {
|
||||
return ((this.value || {}).usage || []).includes('api')
|
||||
},
|
||||
set (value) {
|
||||
this.value.usage = value ? ['api'] : []
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { default as component } from './CSystemEditorAuth.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
settings: {
|
||||
'auth.internal.enabled': true,
|
||||
'auth.internal.signup.enabled': true,
|
||||
},
|
||||
processing: false,
|
||||
success: false,
|
||||
canManage: true,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'System editor auth',
|
||||
group: ['Settings'],
|
||||
component,
|
||||
props,
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('CanManage', 'canManage'),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
settings: {
|
||||
'auth.internal.enabled': false,
|
||||
'auth.internal.signup.enabled': false,
|
||||
},
|
||||
canManage: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-edit-authentication"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', authSettings)"
|
||||
>
|
||||
<h5>
|
||||
{{ $t('internal.title') }}
|
||||
</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.enabled')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.internal.enabled']"
|
||||
switch
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.password-reset.enabled')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.internal.password-reset.enabled']"
|
||||
switch
|
||||
data-test-id="checkbox-password-reset"
|
||||
:value="true"
|
||||
:labels="checkboxLabel"
|
||||
:unchecked-value="false"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.signup.email-confirmation-required')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.internal.signup.email-confirmation-required']"
|
||||
switch
|
||||
:value="true"
|
||||
:labels="checkboxLabel"
|
||||
:unchecked-value="false"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.signup.enabled')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.internal.signup.enabled']"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.profile-avatar.enabled')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.internal.profile-avatar.enabled']"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.signup.split-credentials-check.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.internal.split-credentials-check']"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h5>
|
||||
{{ $t('internal.password-constraints.title') }}
|
||||
</h5>
|
||||
|
||||
<b-alert
|
||||
v-if="!$Settings.get('auth.internal.passwordConstraints.passwordSecurity')"
|
||||
variant="warning"
|
||||
show
|
||||
>
|
||||
{{ $t('internal.password-constraints.ignored-security') }}
|
||||
</b-alert>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.password-constraints.min-upper-case-length')"
|
||||
:description="$t('internal.password-constraints.min-upper-case-description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model.number="authSettings['auth.internal.password-constraints.min-upper-case']"
|
||||
type="number"
|
||||
:placeholder="`${defaultMinUppCaseChrs}`"
|
||||
:min="defaultMinUppCaseChrs"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.password-constraints.min-lower-case-length')"
|
||||
:description="$t('internal.password-constraints.min-lower-case-description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model.number="authSettings['auth.internal.password-constraints.min-lower-case']"
|
||||
type="number"
|
||||
:placeholder="`${defaultMinLowCaseChrs}`"
|
||||
:min="defaultMinLowCaseChrs"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.password-constraints.min-length')"
|
||||
:description="$t('internal.password-constraints.min-length-description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model.number="authSettings['auth.internal.password-constraints.min-length']"
|
||||
:placeholder="`${defaultMinPwd}`"
|
||||
:min="defaultMinPwd"
|
||||
type="number"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.password-constraints.min-num-count')"
|
||||
:description="$t('internal.password-constraints.min-num-count-description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model.number="authSettings['auth.internal.password-constraints.min-num-count']"
|
||||
placeholder="0"
|
||||
min="0"
|
||||
type="number"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.password-constraints.min-special-count')"
|
||||
:description="$t('internal.password-constraints.min-special-count-description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model.number="authSettings['auth.internal.password-constraints.min-special-count']"
|
||||
placeholder="0"
|
||||
min="0"
|
||||
type="number"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('mfa.emailOTP.enabled')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.multi-factor.email-otp.enabled']"
|
||||
data-test-id="checkbox-enable-emailOTP"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
@change="handleEmailOTPEnabledChange"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('mfa.emailOTP.expires.label')"
|
||||
:description="$t('mfa.emailOTP.expires.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group append="seconds">
|
||||
<b-form-input
|
||||
v-model="authSettings['auth.multi-factor.email-otp.expires']"
|
||||
type="number"
|
||||
placeholder="60"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="authSettings['auth.multi-factor.email-otp.enabled']"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('mfa.emailOTP.enforced')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.multi-factor.email-otp.enforced']"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
:disabled="!authSettings['auth.multi-factor.email-otp.enabled']"
|
||||
switch
|
||||
@change="handleEmailOTPEnforcedChange"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('mfa.TOTP.enabled')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.multi-factor.totp.enabled']"
|
||||
data-test-id="checkbox-enable-TOTP"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
@change="handleTOTPEnabledChange"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('mfa.TOTP.issuer.label')"
|
||||
:description="$t('mfa.TOTP.issuer.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model="authSettings['auth.multi-factor.totp.issuer']"
|
||||
placeholder="Corteza"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="authSettings['auth.multi-factor.totp.enabled']"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('mfa.TOTP.enforced')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.multi-factor.totp.enforced']"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
:disabled="!authSettings['auth.multi-factor.totp.enabled']"
|
||||
switch
|
||||
@change="handleTOTPEnforcedChange"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h5>
|
||||
{{ $t('mail.title') }}
|
||||
</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('mail.from-address')"
|
||||
:description="$t('mail.validate-email')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model="authSettings['auth.mail.from-address']"
|
||||
type="email"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('mail.from-name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model="authSettings['auth.mail.from-name']" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h5>
|
||||
{{ $t('internal.send-user-invite-email.title') }}
|
||||
</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.send-user-invite-email.enabled')"
|
||||
:description="$t('internal.send-user-invite-email.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.internal.send-user-invite-email.enabled']"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('internal.send-user-invite-email.expires.label')"
|
||||
:description="$t('internal.send-user-invite-email.expires.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group append="hours">
|
||||
<b-form-input
|
||||
v-model="authSettings['auth.internal.send-user-invite-email.expires']"
|
||||
type="number"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h5>
|
||||
{{ $t('auto-logout.title') }}
|
||||
</h5>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('auto-logout.enabled.label')"
|
||||
:description="$t('auto-logout.enabled.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="authSettings['auth.auto-logout.enabled']"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('auto-logout.timeout.label')"
|
||||
:description="$t('auto-logout.timeout.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group append="seconds">
|
||||
<b-form-input
|
||||
v-model="authSettings['auth.auto-logout.timeout']"
|
||||
type="number"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
v-if="canManage"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', authSettings)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'CSystemEditorAuth',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.settings',
|
||||
keyPrefix: 'editor.auth',
|
||||
},
|
||||
|
||||
props: {
|
||||
settings: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canManage: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
authSettings: {},
|
||||
|
||||
defaultMinPwd: 8,
|
||||
defaultMinUppCaseChrs: 0,
|
||||
defaultMinLowCaseChrs: 0,
|
||||
checkboxLabel: {
|
||||
on: this.$t('general:label.general.yes'),
|
||||
off: this.$t('general:label.general.no'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
settings: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler () {
|
||||
this.authSettings = this.settings
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleEmailOTPEnabledChange (value) {
|
||||
if (!value) {
|
||||
this.authSettings['auth.multi-factor.email-otp.enforced'] = false
|
||||
}
|
||||
},
|
||||
|
||||
handleTOTPEnabledChange (value) {
|
||||
if (!value) {
|
||||
this.authSettings['auth.multi-factor.totp.enforced'] = false
|
||||
}
|
||||
},
|
||||
|
||||
handleEmailOTPEnforcedChange (value) {
|
||||
if (value) {
|
||||
this.authSettings['auth.multi-factor.email-otp.enforced'] = true
|
||||
}
|
||||
},
|
||||
|
||||
handleTOTPEnforcedChange (value) {
|
||||
if (value) {
|
||||
this.authSettings['auth.multi-factor.totp.enforced'] = true
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t("title") }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
label-class="d-flex align-items-center text-primary"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('image.uploader.label') }}
|
||||
|
||||
<c-input-confirm
|
||||
v-if="uploadedFile('auth.ui.background-image-src')"
|
||||
show-icon
|
||||
class="ml-auto"
|
||||
@confirmed="$emit('resetAttachment', 'auth.ui.background-image-src')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<c-uploader-with-preview
|
||||
:value="uploadedFile('auth.ui.background-image-src')"
|
||||
endpoint="/settings/auth.ui.background-image-src"
|
||||
:disabled="!canManage"
|
||||
@upload="$emit('onUpload')"
|
||||
@clear="$emit('resetAttachment', 'auth.ui.background-image-src')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('image.editor.label')"
|
||||
label-class="d-flex align-items-center text-primary"
|
||||
>
|
||||
<c-ace-editor
|
||||
v-model="settings['auth.ui.styles']"
|
||||
data-test-id="auth-bg-image-styling-editor"
|
||||
name="editor/css"
|
||||
lang="css"
|
||||
min-height="500px"
|
||||
show-line-numbers
|
||||
auto-complete
|
||||
:auto-complete-suggestions="customCssAutocompleteVal"
|
||||
resizable
|
||||
class="flex-fill w-100"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
v-if="canManage"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', settings['auth.ui.styles'])"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import { CUSTOM_CSS_AUTO_COMPLETE_VALUES } from 'corteza-webapp-admin/src/lib/cssAutoComplete'
|
||||
import CUploaderWithPreview from 'corteza-webapp-admin/src/components/CUploaderWithPreview'
|
||||
|
||||
const { CAceEditor } = components
|
||||
|
||||
export default {
|
||||
name: 'CSystemEditorAuthBgImage',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.settings',
|
||||
keyPrefix: 'editor.bgScreen',
|
||||
},
|
||||
|
||||
components: {
|
||||
CUploaderWithPreview,
|
||||
CAceEditor,
|
||||
},
|
||||
|
||||
props: {
|
||||
settings: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
canManage: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
customCssAutocompleteVal: CUSTOM_CSS_AUTO_COMPLETE_VALUES,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
uploadedFile (name) {
|
||||
const localAttachment = /^attachment:(\d+)/
|
||||
|
||||
switch (true) {
|
||||
case this.settings[name] && localAttachment.test(this.settings[name]):
|
||||
const [, attachmentID] = localAttachment.exec(this.settings[name])
|
||||
|
||||
return (
|
||||
this.$SystemAPI.baseURL +
|
||||
this.$SystemAPI.attachmentOriginalEndpoint({
|
||||
attachmentID,
|
||||
kind: 'settings',
|
||||
name,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<b-card
|
||||
body-class="p-0"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<div class="d-flex align-items-center flex-grow-1 flex-wrap flex-fill-child gap-1 p-3">
|
||||
<b-button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
@click="newOIDC()"
|
||||
>
|
||||
{{ $t('oidc.add') }}
|
||||
</b-button>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('enabled')"
|
||||
label-class="text-primary"
|
||||
class="mb-0 ml-auto"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="external.enabled"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:labels="checkboxLabel"
|
||||
switch
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
|
||||
<b-table
|
||||
:items="providerItems"
|
||||
:fields="providerFields"
|
||||
:tbody-tr-class="(i) => i.rowBackground"
|
||||
head-variant="light"
|
||||
hover
|
||||
class="mb-0"
|
||||
>
|
||||
<template #cell(enabled)="{ item }">
|
||||
<b-checkbox
|
||||
:checked="item.enabled"
|
||||
@change="item.enable($event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(provider)="{ item }">
|
||||
{{ item.provider || item.tag }}
|
||||
</template>
|
||||
|
||||
<template #cell(editor)="{ item }">
|
||||
<c-input-confirm
|
||||
v-if="item.delete"
|
||||
:icon="item.deleted ? ['fas', 'trash-restore'] : undefined"
|
||||
:variant="item.deleted ? 'outline-warning' : 'outline-danger'"
|
||||
:variant-ok="item.deleted ? 'warning' : 'danger'"
|
||||
@confirmed="item.delete()"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
variant="link"
|
||||
@click="openEditor(item.editor)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'wrench']"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<b-modal
|
||||
v-model="modal.open"
|
||||
:title="modal.title"
|
||||
scrollable
|
||||
size="lg"
|
||||
title-class="text-capitalize"
|
||||
@ok="modal.updater(modal.data)"
|
||||
>
|
||||
<component
|
||||
:is="modal.component"
|
||||
v-model="modal.data"
|
||||
/>
|
||||
</b-modal>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:disabled="!dirty || !canManage"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', changes)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import OidcExternal from 'corteza-webapp-admin/src/components/Settings/System/Auth/ExternalOIDC'
|
||||
import StandardExternal from 'corteza-webapp-admin/src/components/Settings/System/Auth/ExternalStd'
|
||||
import SamlExternal from 'corteza-webapp-admin/src/components/Settings/System/Auth/ExternalSAML'
|
||||
|
||||
const idpStandard = [
|
||||
'google',
|
||||
'github',
|
||||
'facebook',
|
||||
'linkedin',
|
||||
]
|
||||
|
||||
const idpSecurity = {
|
||||
permittedRoles: [],
|
||||
prohibitedRoles: [],
|
||||
forcedRoles: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* Give some structure to key-val settings
|
||||
*/
|
||||
function prepareExternal (external) {
|
||||
const extractKey = (name, t = 'string') => {
|
||||
const v = external.find(v => v.name === `auth.external.${name}`)
|
||||
|
||||
switch (t) {
|
||||
case 'string':
|
||||
return (v || { value: null }).value || ''
|
||||
case 'boolean':
|
||||
return !!(v || { value: null }).value
|
||||
case 'array':
|
||||
return (v || { value: [] }).value || []
|
||||
case undefined:
|
||||
// use t=undefined to get raw value
|
||||
return v.value
|
||||
}
|
||||
}
|
||||
|
||||
const extractKeys = (provider, base = {}) => {
|
||||
const out = { ...base }
|
||||
|
||||
for (const k in base) {
|
||||
out[k] = extractKey(`providers.${provider}.${k}`, Array.isArray(out[k]) ? 'array' : typeof out[k])
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// careful with prefix!
|
||||
// ....providers.openid-connect....
|
||||
// ....providers....
|
||||
// ....saml
|
||||
const extractSec = (prefix) => {
|
||||
return { ...idpSecurity, ...(extractKey(`${prefix}.security`, undefined) || {}) }
|
||||
}
|
||||
|
||||
const data = {
|
||||
enabled: !!(external.find(v => v.name === 'auth.external.enabled') || {}).value,
|
||||
|
||||
oidc: [],
|
||||
standard: [],
|
||||
|
||||
saml: {
|
||||
enabled: extractKey('saml.enabled'),
|
||||
cert: extractKey('saml.cert'),
|
||||
name: extractKey('saml.name'),
|
||||
key: extractKey('saml.key'),
|
||||
'sign-method': extractKey('saml.sign-method'),
|
||||
'sign-requests': extractKey('saml.sign-requests', 'boolean'),
|
||||
binding: extractKey('saml.binding'),
|
||||
idp: {
|
||||
url: extractKey('saml.idp.url'),
|
||||
'ident-name': extractKey('saml.idp.ident-name'),
|
||||
'ident-handle': extractKey('saml.idp.ident-handle'),
|
||||
'ident-identifier': extractKey('saml.idp.ident-identifier'),
|
||||
},
|
||||
security: extractSec('saml'),
|
||||
},
|
||||
}
|
||||
|
||||
data.standard = idpStandard.map(handle => ({
|
||||
handle,
|
||||
...extractKeys(handle, {
|
||||
enabled: false,
|
||||
secret: '',
|
||||
key: '',
|
||||
security: {},
|
||||
usage: [],
|
||||
}),
|
||||
security: extractSec(`providers.${handle}`),
|
||||
}))
|
||||
|
||||
const prefix = 'auth.external.providers.openid-connect.'
|
||||
|
||||
data.oidc =
|
||||
[...new Set(external
|
||||
// Filter out all keys that start with prefix
|
||||
.filter(v => v.name.indexOf(prefix) === 0)
|
||||
// trim off the prefix and everything after next.
|
||||
.map(({ name }) => name.substring(prefix.length).split('.', 2)[0]))]
|
||||
.map(handle => ({
|
||||
...extractKeys('openid-connect.' + handle, {
|
||||
enabled: false,
|
||||
issuer: '',
|
||||
key: '',
|
||||
secret: '',
|
||||
scope: '',
|
||||
security: {},
|
||||
}),
|
||||
handle,
|
||||
security: extractSec('providers.openid-connect.' + handle),
|
||||
deleted: false,
|
||||
}))
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'CSystemEditorExternal',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.settings',
|
||||
keyPrefix: 'editor.external',
|
||||
},
|
||||
|
||||
components: {
|
||||
OidcExternal,
|
||||
StandardExternal,
|
||||
SamlExternal,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canManage: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
// current open modal
|
||||
modal: {
|
||||
open: false,
|
||||
editor: null,
|
||||
title: null,
|
||||
data: null,
|
||||
},
|
||||
|
||||
// working copy
|
||||
external: prepareExternal(this.value),
|
||||
|
||||
roles: [],
|
||||
checkboxLabel: {
|
||||
on: this.$t('general:label.general.yes'),
|
||||
off: this.$t('general:label.general.no'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
dirty () {
|
||||
return this.changes.length > 0
|
||||
},
|
||||
|
||||
// copy of all values for comparison
|
||||
original () {
|
||||
return Object.freeze(prepareExternal(this.value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Configuration for table that shows external providers
|
||||
*
|
||||
* @returns object
|
||||
*/
|
||||
providerFields () {
|
||||
return [
|
||||
{ key: 'enabled', label: this.$t('table.header.enabled'), thStyle: { width: '50px' } },
|
||||
{ key: 'provider', label: this.$t('table.header.provider'), thStyle: { width: '200px' }, tdClass: 'text-capitalize' },
|
||||
{ key: 'info', label: this.$t('table.header.info') },
|
||||
{ key: 'editor', label: '', thStyle: { width: '200px' }, tdClass: 'text-right' },
|
||||
]
|
||||
},
|
||||
|
||||
providerItems () {
|
||||
return [
|
||||
{
|
||||
rowBackground: _.isEqual(this.original.saml, this.external.saml) ? '' : 'bg-extra-light',
|
||||
provider: this.external.saml.name,
|
||||
info: this.external.saml.idp.url,
|
||||
tag: 'SAML',
|
||||
|
||||
enabled: this.external.saml.enabled,
|
||||
enable: (val) => this.$set(this.external.saml, 'enabled', val),
|
||||
|
||||
editor: {
|
||||
component: 'saml-external',
|
||||
data: this.external.saml,
|
||||
title: this.$t('saml.title'),
|
||||
updater: (changed) => this.updater('saml', changed),
|
||||
},
|
||||
},
|
||||
...this.external.oidc
|
||||
.map((p, i) => ({
|
||||
rowBackground: (() => {
|
||||
if (_.isEqual(this.original.oidc[i], p)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (p.deleted) {
|
||||
return 'text-extra-light deleted'
|
||||
}
|
||||
|
||||
return 'bg-extra-light'
|
||||
})(),
|
||||
provider: p.handle,
|
||||
tag: 'OIDC',
|
||||
info: p.issuer,
|
||||
|
||||
enabled: p.enabled,
|
||||
deleted: p.deleted,
|
||||
enable: (val) => this.$set(this.external.oidc[i], 'enabled', val),
|
||||
delete: () => {
|
||||
this.$set(this.external.oidc[i], 'deleted', !p.deleted)
|
||||
},
|
||||
|
||||
editor: {
|
||||
component: 'oidc-external',
|
||||
data: p,
|
||||
title: p.handle,
|
||||
updater: (changed) => this.updater('oidc', changed, i),
|
||||
},
|
||||
})),
|
||||
...this.external.standard.map((p, i) => ({
|
||||
rowBackground: _.isEqual(this.original.standard[i], p) ? '' : 'bg-extra-light',
|
||||
provider: p.handle,
|
||||
info: p.key,
|
||||
|
||||
enabled: p.enabled,
|
||||
enable: (val) => this.$set(this.external.standard[i], 'enabled', val),
|
||||
|
||||
editor: {
|
||||
component: 'standard-external',
|
||||
data: p,
|
||||
title: p.handle,
|
||||
updater: (changed) => this.updater('standard', changed, i),
|
||||
},
|
||||
})),
|
||||
]
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts changed settings back to values
|
||||
*/
|
||||
changes () {
|
||||
let name, value
|
||||
const c = []
|
||||
|
||||
const prefix = 'auth.external.providers'
|
||||
const o = this.original
|
||||
const e = this.external
|
||||
|
||||
if (!_.isEqual(o.enabled, e.enabled)) {
|
||||
c.push({ name: 'auth.external.enabled', value: e.enabled })
|
||||
}
|
||||
|
||||
/**
|
||||
* General purpose key mapper
|
||||
*/
|
||||
const mapKeys = (prefix, wc, org, keys) => {
|
||||
for (const k of keys) {
|
||||
if (wc[k] === undefined) {
|
||||
console.error(`potential issue - unknown key "${prefix}.${k}" used`, wc)
|
||||
}
|
||||
|
||||
if (_.isEqual(wc[k], org[k])) {
|
||||
continue
|
||||
}
|
||||
|
||||
name = `${prefix}.${k}`
|
||||
value = wc[k]
|
||||
c.push({ name, value })
|
||||
}
|
||||
}
|
||||
|
||||
e.standard.forEach((p, i) => {
|
||||
mapKeys(
|
||||
`${prefix}.${p.handle}`,
|
||||
p,
|
||||
o.standard[i],
|
||||
['key', 'secret', 'enabled', 'security', 'usage'],
|
||||
)
|
||||
})
|
||||
|
||||
// @todo how do we remove OIDC?
|
||||
const oidcKeys = ['key', 'secret', 'enabled', 'issuer', 'scope', 'security']
|
||||
e.oidc.forEach((p, i) => {
|
||||
if (p.deleted) {
|
||||
// use base set of keys used for OIDC provider
|
||||
// plus add a few extra ones that are usually set by the backend
|
||||
//
|
||||
// @todo it would probably smarter to trigger the backend that all settings
|
||||
// with a certain key prefix should be deleted.
|
||||
// Unfortunately, backend does not support that for now + could be a bit dangerous
|
||||
[...oidcKeys, 'weight', 'redirect', 'label']
|
||||
.forEach(name => c.push({ name: `${prefix}.openid-connect.${p.handle}.${name}`, value: null }))
|
||||
} else {
|
||||
mapKeys(
|
||||
`${prefix}.openid-connect.${p.handle}`,
|
||||
p,
|
||||
o.oidc[i] || {},
|
||||
oidcKeys,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
mapKeys(
|
||||
'auth.external.saml',
|
||||
e.saml,
|
||||
o.saml,
|
||||
['enabled', 'name', 'key', 'cert', 'sign-method', 'sign-requests', 'binding', 'security'],
|
||||
)
|
||||
|
||||
mapKeys(
|
||||
'auth.external.saml.idp',
|
||||
e.saml.idp,
|
||||
o.saml.idp,
|
||||
['url', 'ident-name', 'ident-handle', 'ident-identifier'],
|
||||
)
|
||||
|
||||
return c
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler () {
|
||||
this.external = prepareExternal(this.value)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
openEditor ({ component, title, data, updater }) {
|
||||
this.modal.open = true
|
||||
this.modal.component = component
|
||||
this.modal.title = title
|
||||
this.modal.updater = updater
|
||||
|
||||
// deref
|
||||
this.modal.data = JSON.parse(JSON.stringify(data))
|
||||
},
|
||||
|
||||
newOIDC () {
|
||||
this.openEditor({
|
||||
component: 'oidc-external',
|
||||
title: this.$t('oidc.add'),
|
||||
data: {
|
||||
handle: '',
|
||||
enabled: true,
|
||||
issuer: '',
|
||||
key: '',
|
||||
secret: '',
|
||||
scope: '',
|
||||
fresh: true,
|
||||
security: { ...idpSecurity },
|
||||
},
|
||||
updater: (changed) => {
|
||||
this.updater('oidc', changed, -1)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Please note that we're assuming that array items will not get changed!
|
||||
*
|
||||
* @param key
|
||||
* @param i
|
||||
* @param val
|
||||
*/
|
||||
updater (key, val, i = undefined) {
|
||||
if (i === undefined) {
|
||||
this.$set(this.external, key, val)
|
||||
} else if (i < 0) {
|
||||
this.external[key].push(val)
|
||||
} else {
|
||||
this.$set(this.external[key], i, val)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,406 @@
|
||||
<template>
|
||||
<b-card
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
body-class="p-0"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="!sassInstalled"
|
||||
class="bg-warning rounded p-2 mb-3"
|
||||
>
|
||||
{{ $t('sassNotInstalled') }}
|
||||
<a
|
||||
:href="installSassDocs"
|
||||
target="_blank"
|
||||
class="text-dark"
|
||||
>
|
||||
{{ $t('installSassDocs') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<b-tabs
|
||||
data-test-id="theme-tabs"
|
||||
card
|
||||
>
|
||||
<b-tab
|
||||
v-for="theme in themes"
|
||||
:key="theme.id"
|
||||
:title="theme.title"
|
||||
>
|
||||
<b-row v-if="theme.id !== 'general'">
|
||||
<b-col
|
||||
v-for="key in themeVariables"
|
||||
:key="key"
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t(`theme.variables.${key}.label`)"
|
||||
label-class="text-primary"
|
||||
:description="$t(`theme.variables.${key}.description`)"
|
||||
>
|
||||
<c-input-color-picker
|
||||
ref="picker"
|
||||
v-model="theme.variables[key]"
|
||||
:default-value="theme.defaultVariables[key]"
|
||||
:data-test-id="`input-${key}-color`"
|
||||
:translations="{
|
||||
modalTitle: $t('colorPicker'),
|
||||
defaultBtnLabel: $t('label.default'),
|
||||
light: $t('tabs.light'),
|
||||
dark: $t('tabs.dark'),
|
||||
cancelBtnLabel: $t('general:label.cancel'),
|
||||
saveBtnLabel: $t('general:label.saveAndClose')
|
||||
}"
|
||||
:theme-settings="settings['ui.studio.themes']"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row v-else>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
label-class="d-flex align-items-center text-primary"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('mainLogo.title') }}
|
||||
|
||||
<c-input-confirm
|
||||
v-if="uploadedFile('ui.main-logo')"
|
||||
show-icon
|
||||
class="ml-auto"
|
||||
@confirmed="resetAttachment('ui.main-logo')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<c-uploader-with-preview
|
||||
:value="uploadedFile('ui.main-logo')"
|
||||
endpoint="/settings/ui.main-logo"
|
||||
:disabled="!canManage"
|
||||
@upload="onUpload"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
label-class="d-flex align-items-center text-primary h-lg-100"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('iconLogo.title') }}
|
||||
|
||||
<c-input-confirm
|
||||
v-if="uploadedFile('ui.icon-logo')"
|
||||
show-icon
|
||||
class="ml-auto"
|
||||
@confirmed="resetAttachment('ui.icon-logo')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<c-uploader-with-preview
|
||||
:value="uploadedFile('ui.icon-logo')"
|
||||
endpoint="/settings/ui.icon-logo"
|
||||
:disabled="!canManage"
|
||||
@upload="onUpload"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('custom-css')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<c-ace-editor
|
||||
v-model="theme.customCSS"
|
||||
auto-complete
|
||||
lang="css"
|
||||
min-height="400px"
|
||||
show-line-numbers
|
||||
:show-popout="true"
|
||||
:auto-complete-suggestions="customCssAutocompleteVal"
|
||||
@open="openCustomCSSModal(theme.id)"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
|
||||
<b-modal
|
||||
id="custom-css-editor"
|
||||
v-model="customCSSModal.show"
|
||||
:title="$t('custom-css')"
|
||||
cancel-variant="light"
|
||||
size="xl"
|
||||
scrollable
|
||||
:ok-title="$t('general:label.saveAndClose')"
|
||||
:cancel-title="$t('general:label.cancel')"
|
||||
body-class="p-0"
|
||||
@ok="saveCustomCSSModal()"
|
||||
@hidden="resetCustomCSSModal()"
|
||||
>
|
||||
<c-ace-editor
|
||||
v-model="customCSSModal.value"
|
||||
auto-complete
|
||||
lang="css"
|
||||
min-height="80vh"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
:show-popout="false"
|
||||
:auto-complete-suggestions="customCssAutocompleteVal"
|
||||
/>
|
||||
</b-modal>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
v-if="canManage"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CUploaderWithPreview from 'corteza-webapp-admin/src/components//CUploaderWithPreview'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import { CUSTOM_CSS_AUTO_COMPLETE_VALUES } from 'corteza-webapp-admin/src/lib/cssAutoComplete'
|
||||
const { CInputColorPicker, CAceEditor } = components
|
||||
|
||||
export default {
|
||||
name: 'CUIBrandingEditor',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'ui.settings',
|
||||
keyPrefix: 'editor.corteza-studio',
|
||||
},
|
||||
|
||||
components: {
|
||||
CUploaderWithPreview,
|
||||
CInputColorPicker,
|
||||
CAceEditor,
|
||||
},
|
||||
|
||||
props: {
|
||||
settings: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canManage: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
themeTabs: [
|
||||
'general',
|
||||
'light',
|
||||
'dark',
|
||||
],
|
||||
themeVariables: [
|
||||
'black',
|
||||
'white',
|
||||
'primary',
|
||||
'secondary',
|
||||
'success',
|
||||
'warning',
|
||||
'danger',
|
||||
'light',
|
||||
'extra-light',
|
||||
'body-bg',
|
||||
'sidebar-bg',
|
||||
'topbar-bg',
|
||||
],
|
||||
lightModeVariables: {
|
||||
black: '#0B344E',
|
||||
white: '#FFFFFF',
|
||||
primary: '#FF9661',
|
||||
secondary: '#758D9B',
|
||||
success: '#43AA8B',
|
||||
warning: '#E27646',
|
||||
danger: '#E54122',
|
||||
light: '#F3F5F7',
|
||||
'extra-light': '#E4E9EF',
|
||||
'body-bg': '#F3F5F7',
|
||||
'sidebar-bg': '#FFFFFF',
|
||||
'topbar-bg': '#F3F5F7',
|
||||
},
|
||||
darkModeVariables: {
|
||||
black: '#FBF7F4',
|
||||
white: '#0B344E',
|
||||
primary: '#FF9661',
|
||||
secondary: '#758D9B',
|
||||
success: '#43AA8B',
|
||||
warning: '#E27646',
|
||||
danger: '#E54122',
|
||||
light: '#23495F',
|
||||
'extra-light': '#3E5A6F',
|
||||
'body-bg': '#092B40',
|
||||
'sidebar-bg': '#0B344E',
|
||||
'topbar-bg': '#092B40',
|
||||
},
|
||||
|
||||
themes: [],
|
||||
themeColors: [],
|
||||
|
||||
customCSSModal: {
|
||||
show: false,
|
||||
id: '',
|
||||
value: '',
|
||||
},
|
||||
|
||||
customCssAutocompleteVal: CUSTOM_CSS_AUTO_COMPLETE_VALUES,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
sassInstalled () {
|
||||
return this.settings['ui.studio.sass-installed']
|
||||
},
|
||||
installSassDocs () {
|
||||
// eslint-disable-next-line no-undef
|
||||
const [year, month] = VERSION.split('.')
|
||||
return `https://docs.cortezaproject.org/corteza-docs/${year}.${month}/integrator-guide/corteza-studio/index.html`
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
settings: {
|
||||
immediate: true,
|
||||
handler (settings) {
|
||||
const themes = settings['ui.studio.themes'] || []
|
||||
const customCSS = settings['ui.studio.custom-css'] || []
|
||||
|
||||
this.themes = this.themeTabs.map((id) => {
|
||||
const { title, values = '' } = themes.find(t => t.id === id) || {}
|
||||
const defaultCustomCSS = customCSS.find(t => t.id === id) || {}
|
||||
|
||||
let variables = JSON.parse(values || '{}')
|
||||
let defaultVariables
|
||||
|
||||
if (['light', 'dark'].includes(id)) {
|
||||
if (!values) {
|
||||
variables = id === 'light' ? this.lightModeVariables : this.darkModeVariables
|
||||
}
|
||||
|
||||
defaultVariables = id === 'light' ? this.lightModeVariables : this.darkModeVariables
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
title: title || this.$t(`tabs.${id}`),
|
||||
variables,
|
||||
defaultVariables,
|
||||
customCSS: defaultCustomCSS.values || '',
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit () {
|
||||
this.$emit('submit', {
|
||||
'ui.studio.themes': this.themes.map(theme => {
|
||||
return {
|
||||
id: theme.id,
|
||||
title: theme.title,
|
||||
values: JSON.stringify(theme.variables),
|
||||
}
|
||||
}),
|
||||
'ui.studio.custom-css': this.themes.map(theme => {
|
||||
return {
|
||||
id: theme.id,
|
||||
title: theme.title,
|
||||
values: theme.customCSS,
|
||||
}
|
||||
}),
|
||||
})
|
||||
},
|
||||
|
||||
openCustomCSSModal (id) {
|
||||
const { customCSS } = this.themes.find(t => t.id === id) || {}
|
||||
|
||||
this.customCSSModal.id = id
|
||||
this.customCSSModal.value = customCSS
|
||||
this.customCSSModal.show = true
|
||||
},
|
||||
|
||||
saveCustomCSSModal () {
|
||||
this.themes.find(t => t.id === this.customCSSModal.id).customCSS = this.customCSSModal.value
|
||||
},
|
||||
|
||||
resetCustomCSSModal () {
|
||||
this.customCSSModal.id = ''
|
||||
this.customCSSModal.value = ''
|
||||
this.customCSSModal.show = false
|
||||
},
|
||||
|
||||
resetColor (key, theme) {
|
||||
this.$set(theme.variables, key, theme.id === 'light' ? this.lightModeVariables[key] : this.darkModeVariables[key])
|
||||
},
|
||||
|
||||
onUpload ({ name, value }) {
|
||||
this.$set(this.settings, name, value)
|
||||
},
|
||||
|
||||
resetAttachment (name) {
|
||||
this.$SystemAPI.settingsUpdate({ values: [{ name, value: undefined }], upload: {} })
|
||||
.then(() => {
|
||||
this.$set(this.settings, name, undefined)
|
||||
})
|
||||
},
|
||||
|
||||
uploadedFile (name) {
|
||||
const localAttachment = /^attachment:(\d+)/
|
||||
|
||||
switch (true) {
|
||||
case this.settings[name] && localAttachment.test(this.settings[name]):
|
||||
const [, attachmentID] = localAttachment.exec(this.settings[name])
|
||||
|
||||
return this.$SystemAPI.baseURL +
|
||||
this.$SystemAPI.attachmentOriginalEndpoint({
|
||||
attachmentID,
|
||||
kind: 'settings',
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('geosearch.title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', settings)"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('geosearch.provider.label')"
|
||||
:description="$t('geosearch.provider.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-select
|
||||
v-model="locationSettings.geoSearchProvider"
|
||||
:options="providerOptions"
|
||||
:reduce="o => o.value"
|
||||
label="text"
|
||||
:clearable="false"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="requiresApiKey"
|
||||
:label="$t('geosearch.apiKey.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="locationSettings.geoSearchApiKey"
|
||||
type="text"
|
||||
:placeholder="$t('geosearch.apiKey.placeholder')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
v-if="canManage"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:disabled="isSubmitDisabled"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CInputSelect } = components
|
||||
|
||||
export default {
|
||||
name: 'CUILocationSettings',
|
||||
|
||||
components: {
|
||||
CInputSelect,
|
||||
},
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'ui.settings',
|
||||
keyPrefix: 'editor.location',
|
||||
},
|
||||
|
||||
props: {
|
||||
settings: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canManage: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
locationSettings: {},
|
||||
|
||||
providerOptions: [
|
||||
{ value: 'openstreetmap', text: 'OpenStreetMap', requiresApiKey: false },
|
||||
{ value: 'opencage', text: 'OpenCage', requiresApiKey: true },
|
||||
{ value: 'esri', text: 'Esri', requiresApiKey: false },
|
||||
{ value: 'geoapify', text: 'Geoapify', requiresApiKey: true },
|
||||
{ value: 'geocodeearth', text: 'Geocode Earth', requiresApiKey: true },
|
||||
{ value: 'google', text: 'Google Maps', requiresApiKey: true },
|
||||
{ value: 'locationiq', text: 'LocationIQ', requiresApiKey: true },
|
||||
{ value: 'mapbox', text: 'Mapbox', requiresApiKey: true },
|
||||
{ value: 'pelias', text: 'Pelias', requiresApiKey: true },
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
selectedProvider () {
|
||||
return this.providerOptions.find(p => p.value === this.locationSettings.geoSearchProvider)
|
||||
},
|
||||
|
||||
requiresApiKey () {
|
||||
return this.selectedProvider?.requiresApiKey || false
|
||||
},
|
||||
|
||||
isSubmitDisabled () {
|
||||
return this.requiresApiKey && !this.locationSettings.geoSearchApiKey
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
settings: {
|
||||
immediate: true,
|
||||
handler (settings) {
|
||||
this.locationSettings = settings['ui.location'] || {}
|
||||
|
||||
if (!this.locationSettings.geoSearchProvider) {
|
||||
this.$set(this.locationSettings, 'geoSearchProvider', 'openstreetmap')
|
||||
}
|
||||
|
||||
if (!this.locationSettings.geoSearchApiKey) {
|
||||
this.$set(this.locationSettings, 'geoSearchApiKey', '')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
'locationSettings.geoSearchProvider' (newProvider) {
|
||||
this.locationSettings.geoSearchApiKey = ''
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit () {
|
||||
this.$emit('submit', { 'ui.location': this.locationSettings })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', settings)"
|
||||
>
|
||||
<h5>{{ $t('general') }}</h5>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideAppSelector"
|
||||
>
|
||||
{{ $t('app-selector.hide') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="hideDrafts"
|
||||
>
|
||||
{{ $t('drafts.hide') }}
|
||||
<b-badge
|
||||
variant="warning"
|
||||
class="ml-1"
|
||||
>
|
||||
{{ $t('general:label.experimental') }}
|
||||
</b-badge>
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideNotifications"
|
||||
>
|
||||
{{ $t('notifications.hide') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideHelp"
|
||||
>
|
||||
{{ $t('help.hide') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-if="$Settings.get('discovery.enabled', false)"
|
||||
v-model="hideSearch"
|
||||
>
|
||||
{{ $t('search.hide') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideProfile"
|
||||
>
|
||||
{{ $t('profile.hide') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<div>
|
||||
<hr>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="3"
|
||||
>
|
||||
<h5>{{ $t('help.title') }}</h5>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideForumLink"
|
||||
>
|
||||
{{ $t('help.hide-forum-link') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideDocumentationLink"
|
||||
>
|
||||
{{ $t('help.hide-documentation-link') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideFeedbackLink"
|
||||
>
|
||||
{{ $t('help.hide-feedback-link') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('links.title')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-form-table-wrapper
|
||||
:labels="{ addButton: $t('general:label.add') }"
|
||||
@add-item="topbarSettings.helpLinks.push({ handle: '', url: '', newTab: true })"
|
||||
>
|
||||
<b-table
|
||||
v-if="topbarSettings.helpLinks.length > 0"
|
||||
:fields="links.fields"
|
||||
:items="topbarSettings.helpLinks"
|
||||
thead-tr-class="text-primary"
|
||||
responsive
|
||||
small
|
||||
>
|
||||
<template #cell(handle)="data">
|
||||
<b-form-input
|
||||
v-model="data.item.handle"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(url)="data">
|
||||
<b-form-input
|
||||
v-model="data.item.url"
|
||||
type="url"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(newTab)="data">
|
||||
<b-form-checkbox
|
||||
v-model="data.item.newTab"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(actions)="data">
|
||||
<c-input-confirm
|
||||
show-icon
|
||||
@confirmed="topbarSettings.helpLinks.splice(data.index, 1)"
|
||||
/>
|
||||
</template>
|
||||
</b-table>
|
||||
</c-form-table-wrapper>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<hr>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="3"
|
||||
>
|
||||
<h5>{{ $t('profile.title') }}</h5>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideProfileLink"
|
||||
>
|
||||
{{ $t('profile.hide-profile-link') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideChangePasswordLink"
|
||||
>
|
||||
{{ $t('profile.hide-change-password-link') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="topbarSettings.hideThemeSelector"
|
||||
>
|
||||
{{ $t('profile.hide-theme-selector') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col>
|
||||
<b-form-group
|
||||
:label="$t('links.title')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-form-table-wrapper
|
||||
:labels="{ addButton: $t('general:label.add') }"
|
||||
@add-item="topbarSettings.profileLinks.push({ handle: '', url: '', newTab: true })"
|
||||
>
|
||||
<b-table
|
||||
v-if="topbarSettings.profileLinks.length > 0"
|
||||
:fields="links.fields"
|
||||
:items="topbarSettings.profileLinks"
|
||||
thead-tr-class="text-primary"
|
||||
responsive
|
||||
small
|
||||
>
|
||||
<template #cell(handle)="data">
|
||||
<b-form-input
|
||||
v-model="data.item.handle"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(url)="data">
|
||||
<b-form-input
|
||||
v-model="data.item.url"
|
||||
type="url"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(newTab)="data">
|
||||
<b-form-checkbox
|
||||
v-model="data.item.newTab"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(actions)="data">
|
||||
<c-input-confirm
|
||||
show-icon
|
||||
@confirmed="topbarSettings.profileLinks.splice(data.index, 1)"
|
||||
/>
|
||||
</template>
|
||||
</b-table>
|
||||
</c-form-table-wrapper>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
v-if="canManage"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'CUITopbarSettings',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'ui.settings',
|
||||
keyPrefix: 'editor.topbar',
|
||||
},
|
||||
|
||||
props: {
|
||||
settings: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canManage: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
topbarSettings: {},
|
||||
|
||||
links: {
|
||||
fields: [
|
||||
{
|
||||
key: 'handle',
|
||||
label: this.$t('links.handle'),
|
||||
thStyle: { width: '30%' },
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: this.$t('links.url'),
|
||||
thStyle: { width: '55%' },
|
||||
},
|
||||
{
|
||||
key: 'newTab',
|
||||
label: this.$t('links.new-tab'),
|
||||
thClass: 'text-center',
|
||||
tdClass: 'text-center align-middle',
|
||||
thStyle: { width: '10%' },
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right align-middle',
|
||||
thStyle: { width: '1%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hideDrafts: {
|
||||
get () {
|
||||
return this.topbarSettings.showDrafts !== true
|
||||
},
|
||||
|
||||
set (value) {
|
||||
this.$set(this.topbarSettings, 'showDrafts', !value)
|
||||
},
|
||||
},
|
||||
|
||||
hideSearch: {
|
||||
get () {
|
||||
return this.topbarSettings.showSearch !== true
|
||||
},
|
||||
|
||||
set (value) {
|
||||
this.$set(this.topbarSettings, 'showSearch', !value)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
settings: {
|
||||
immediate: true,
|
||||
handler (settings) {
|
||||
this.topbarSettings = settings['ui.topbar'] || {}
|
||||
|
||||
if (!this.topbarSettings.helpLinks) {
|
||||
this.$set(this.topbarSettings, 'helpLinks', [])
|
||||
}
|
||||
|
||||
if (!this.topbarSettings.profileLinks) {
|
||||
this.$set(this.topbarSettings, 'profileLinks', [])
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit () {
|
||||
this.$emit('submit', { 'ui.topbar': this.topbarSettings })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
56
client/web/admin/src/components/Template/C3.js
Normal file
56
client/web/admin/src/components/Template/C3.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { default as component } from './CTemplateEditorInfo.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
template: {
|
||||
templateID: '',
|
||||
meta: {
|
||||
short: 'Name',
|
||||
description: 'Desc',
|
||||
},
|
||||
handle: 'Handle',
|
||||
},
|
||||
processing: false,
|
||||
success: false,
|
||||
canCreate: true,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Editor info',
|
||||
group: ['Templates'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('CanCreate', 'canCreate'),
|
||||
checkbox('Enable delete', {
|
||||
value (p) { return p.template.templateID.length > 0 },
|
||||
update (p, val) { p.template.templateID = val ? '123456789' : '' },
|
||||
}),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
template: {
|
||||
...props.template,
|
||||
meta: {
|
||||
short: '',
|
||||
description: '',
|
||||
},
|
||||
handle: '',
|
||||
},
|
||||
canCreate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<c-ace-editor
|
||||
v-model="template.template"
|
||||
data-test-id="template-html-editor"
|
||||
name="editor/html"
|
||||
lang="html"
|
||||
min-height="400px"
|
||||
show-line-numbers
|
||||
highlight-active-line
|
||||
show-print-margin
|
||||
:border="false"
|
||||
resizable
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CAceEditor } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CAceEditor,
|
||||
},
|
||||
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<c-ace-editor
|
||||
v-model="template.template"
|
||||
data-test-id="template-plain-text-editor"
|
||||
name="editor/plain_text"
|
||||
lang="plain_text"
|
||||
min-height="400px"
|
||||
show-line-numbers
|
||||
resizable
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CAceEditor } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CAceEditor,
|
||||
},
|
||||
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-template-toolbox"
|
||||
no-body
|
||||
header-class="border-bottom"
|
||||
class="shadow-sm h-100"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-card-body>
|
||||
<span
|
||||
v-for="sec in sections"
|
||||
:key="sec.key"
|
||||
>
|
||||
<b-btn
|
||||
:data-test-id="toolboxSectionLabelCypressId(sec.key)"
|
||||
variant="light"
|
||||
class="mb-2"
|
||||
block
|
||||
@click="openSection(sec.key)"
|
||||
>
|
||||
{{ $t(sec.key) }}
|
||||
</b-btn>
|
||||
<b-collapse
|
||||
:visible="expandedSections[sec.key]"
|
||||
role="tabpanel"
|
||||
class="pb-2 px-0"
|
||||
>
|
||||
<b-list-group flush>
|
||||
<b-list-group-item
|
||||
v-for="(opt, i) in sec.options"
|
||||
:key="opt.label + i"
|
||||
:data-test-id="toolboxOptionLabelCypressId(opt.label)"
|
||||
class="px-0 text-wrap"
|
||||
@click="opt.onClick || (() => {})"
|
||||
>
|
||||
{{ opt.label }}
|
||||
<b-btn
|
||||
data-test-id="button-copy"
|
||||
variant="link"
|
||||
class="pr-0 float-right"
|
||||
@click="copyToCb(opt.copyValue())"
|
||||
>
|
||||
<font-awesome-icon
|
||||
v-if="opt.copyValue"
|
||||
:icon="['far', 'copy']"
|
||||
/>
|
||||
</b-btn>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-collapse>
|
||||
|
||||
</span>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.templates',
|
||||
keyPrefix: 'editor.content.toolbox',
|
||||
},
|
||||
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
partials: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
expandedSections: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
sections () {
|
||||
const partials = this.partials.map(p => ({
|
||||
label: p.meta.short || p.handle,
|
||||
copyValue: () => `{{template "${p.handle}" }}`,
|
||||
}))
|
||||
|
||||
const rr = []
|
||||
if (partials.length) {
|
||||
rr.push({
|
||||
key: 'partials',
|
||||
options: partials,
|
||||
})
|
||||
}
|
||||
|
||||
rr.push({
|
||||
key: 'snippets.label',
|
||||
options: [
|
||||
{
|
||||
label: this.$t('snippets.interpolate'),
|
||||
copyValue: () => '{{.parameter}}',
|
||||
},
|
||||
{
|
||||
label: this.$t('snippets.iterator'),
|
||||
copyValue: () => '{{range $index, $element := .ListOfItems}}\n\n{{end}}',
|
||||
},
|
||||
{
|
||||
label: this.$t('snippets.funcCall'),
|
||||
copyValue: () => '{{funcName param1 param2 paramN}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'samples.label',
|
||||
options: [
|
||||
{
|
||||
label: this.$t('samples.defaultHTML'),
|
||||
copyValue: () => `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
|
||||
<title>Title</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
</body>
|
||||
</html>`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return rr
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
openSection (sec) {
|
||||
this.$set(this.expandedSections, sec, !this.expandedSections[sec])
|
||||
},
|
||||
|
||||
copyToCb: copy,
|
||||
|
||||
toolboxSectionLabelCypressId (section) {
|
||||
return `button-${this.$t(section).toLowerCase()}`
|
||||
},
|
||||
|
||||
toolboxOptionLabelCypressId (label) {
|
||||
return label.toLowerCase().split(' ').join('-')
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<p>
|
||||
{{ $t('editor.unsupported') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.templates',
|
||||
keyPrefix: 'editor.content.editor',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<b-row>
|
||||
<!-- Toolbox -->
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="3"
|
||||
class="mb-3"
|
||||
>
|
||||
<editor-toolbox
|
||||
:template="template"
|
||||
:partials="partials"
|
||||
/>
|
||||
</b-col>
|
||||
|
||||
<!-- Preview configuration -->
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="9"
|
||||
class="mb-3"
|
||||
>
|
||||
<b-card
|
||||
v-if="!template.partial && !fresh"
|
||||
body-class="p-0"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex justify-content-end flex-wrap flex-fill-child gap-1"
|
||||
>
|
||||
<!-- Partial templates can't be previewed -->
|
||||
<c-ace-editor
|
||||
v-model="previewData"
|
||||
data-test-id="template-preview-output"
|
||||
name="preview-data"
|
||||
lang="json"
|
||||
min-height="300px"
|
||||
show-line-numbers
|
||||
highlight-active-line
|
||||
resizable
|
||||
:border="false"
|
||||
/>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('preview.title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<b-btn
|
||||
v-if="canPreviewHTML"
|
||||
data-test-id="button-preview-html-template"
|
||||
variant="light"
|
||||
@click="openPreview('html')"
|
||||
>
|
||||
{{ $t('preview.html') }}
|
||||
</b-btn>
|
||||
|
||||
<b-btn
|
||||
v-if="canPreviewPDF"
|
||||
data-test-id="button-preview-pdf-template"
|
||||
variant="light"
|
||||
@click="openPreview('pdf')"
|
||||
>
|
||||
{{ $t('preview.pdf') }}
|
||||
</b-btn>
|
||||
</template>
|
||||
</b-card>
|
||||
</b-col>
|
||||
|
||||
<!-- Content editor -->
|
||||
<b-col cols="12">
|
||||
<b-card
|
||||
body-class="p-0"
|
||||
header-class="d-flex align-items-center border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-row
|
||||
v-if="!template.partial"
|
||||
class="p-3 border-bottom no-gutters"
|
||||
>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
class="pr-lg-2"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('headerTemplate')"
|
||||
:description="$t('headerTemplateDescription')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="headerTemplateID"
|
||||
data-test-id="select-header-template"
|
||||
:options="partialOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
class="pl-lg-2"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('footerTemplate')"
|
||||
:description="$t('footerTemplateDescription')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="footerTemplateID"
|
||||
data-test-id="select-footer-template"
|
||||
:options="partialOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<component
|
||||
:is="editor"
|
||||
:template="template"
|
||||
/>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
<b-badge
|
||||
v-if="template.partial"
|
||||
data-test-id="badge-partial-template"
|
||||
variant="primary"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ $t('partial') }}
|
||||
</b-badge>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:disabled="!canCreate"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', template)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
import listHelpers from 'corteza-webapp-admin/src/mixins/listHelpers'
|
||||
import EditorToolbox from './EditorToolbox'
|
||||
import EditorTextHtml from './EditorTextHtml'
|
||||
import EditorTextPlain from './EditorTextPlain'
|
||||
import EditorUnsupported from './EditorUnsupported'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CAceEditor } = components
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
CAceEditor,
|
||||
EditorToolbox,
|
||||
},
|
||||
mixins: [
|
||||
listHelpers,
|
||||
],
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.templates',
|
||||
keyPrefix: 'editor.content',
|
||||
},
|
||||
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
partials: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
// @todo i18n this?
|
||||
previewData: '{\n "variables": {\n "param1": "value1",\n "param2": {\n "nestedParam1": "value2"\n }\n },\n "options": {\n "documentSize": "A4",\n "contentScale": "1",\n "orientation": "portrait",\n "margin": "0.3"\n }\n}\n',
|
||||
|
||||
previewBlob: '',
|
||||
|
||||
availableDrivers: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return !this.template.templateID || this.template.templateID === NoID
|
||||
},
|
||||
|
||||
partialOptions () {
|
||||
const options = this.partials
|
||||
.filter(({ templateID }) => templateID !== this.template.templateID)
|
||||
.map(({ templateID, meta = {}, handle: h }) => ({ value: templateID, text: meta.short || h || templateID }))
|
||||
|
||||
return [{ value: NoID, text: this.$t('general:label.none') }, ...options]
|
||||
},
|
||||
|
||||
headerTemplateID: {
|
||||
get () {
|
||||
return this.template.meta.headerTemplateID || NoID
|
||||
},
|
||||
set (headerTemplateID) {
|
||||
this.$set(this.template.meta, 'headerTemplateID', headerTemplateID)
|
||||
},
|
||||
},
|
||||
|
||||
footerTemplateID: {
|
||||
get () {
|
||||
return this.template.meta.footerTemplateID || NoID
|
||||
},
|
||||
set (footerTemplateID) {
|
||||
this.$set(this.template.meta, 'footerTemplateID', footerTemplateID)
|
||||
},
|
||||
},
|
||||
|
||||
editor () {
|
||||
switch (this.template.type) {
|
||||
case 'text/html':
|
||||
return EditorTextHtml
|
||||
case 'text/plain':
|
||||
return EditorTextPlain
|
||||
default:
|
||||
return EditorUnsupported
|
||||
}
|
||||
},
|
||||
|
||||
canPreviewHTML () {
|
||||
return this.availableDrivers.find(({ outputTypes }) => outputTypes.includes('text/html'))
|
||||
},
|
||||
|
||||
canPreviewPDF () {
|
||||
return this.availableDrivers.find(({ outputTypes }) => outputTypes.includes('application/pdf'))
|
||||
},
|
||||
},
|
||||
|
||||
async created () {
|
||||
this.availableDrivers = await this.$SystemAPI.templateRenderDrivers()
|
||||
.then(rsp => rsp.set)
|
||||
.catch(this.toastErrorHandler(this.$t('notification:template.driver.error')))
|
||||
},
|
||||
|
||||
methods: {
|
||||
openPreview (ext) {
|
||||
this.incLoader()
|
||||
|
||||
const cfg = {
|
||||
method: 'post',
|
||||
responseType: 'blob',
|
||||
url: this.$SystemAPI.templateRenderEndpoint({
|
||||
templateID: this.template.templateID,
|
||||
filename: 'preview',
|
||||
ext,
|
||||
}),
|
||||
data: JSON.parse(this.previewData),
|
||||
}
|
||||
|
||||
this.$SystemAPI.api().request(cfg)
|
||||
.then(r => {
|
||||
this.previewBlob = window.URL.createObjectURL(r.data)
|
||||
window.open(this.previewBlob, '_newtab')
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:template.preview.error')))
|
||||
.finally(() => {
|
||||
this.decLoader()
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
226
client/web/admin/src/components/Template/CTemplateEditorInfo.vue
Normal file
226
client/web/admin/src/components/Template/CTemplateEditorInfo.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-template-info"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit="$emit('submit', template)"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('meta.short')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="template.meta.short"
|
||||
data-test-id="input-short-name"
|
||||
required
|
||||
:state="shortState"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('handle')"
|
||||
label-class="text-primary"
|
||||
:class="{ 'mb-0': !template.templateID }"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="template.handle"
|
||||
data-test-id="input-handle"
|
||||
:state="handleState"
|
||||
:placeholder="$t('placeholder-handle')"
|
||||
/>
|
||||
|
||||
<b-form-invalid-feedback :state="handleState">
|
||||
{{ $t('invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('type')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="template.type"
|
||||
data-test-id="select-template-type"
|
||||
:options="contentTypes"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('meta.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="template.meta.description"
|
||||
data-test-id="textarea-description"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('partial')"
|
||||
:description="$t('partialDescription')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-checkbox
|
||||
v-model="template.partial"
|
||||
data-test-id="checkbox-is-partial-template"
|
||||
switch
|
||||
:labels="checkboxLabel"
|
||||
name="checkbox-1"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:resource="template"
|
||||
/>
|
||||
|
||||
<!--
|
||||
include hidden input to enable
|
||||
trigger submit event w/ ENTER
|
||||
-->
|
||||
<input
|
||||
type="submit"
|
||||
class="d-none"
|
||||
:disabled="saveDisabled"
|
||||
>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="!fresh && template.canDeleteTemplate"
|
||||
:data-test-id="getDeletedButtonStatusCypressId"
|
||||
:text="getDeleteStatus"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', template)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
import { handle } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export default {
|
||||
name: 'CTemplateEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.templates',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
contentTypes: [
|
||||
{ value: 'text/html', text: this.$t('contentType.text_html') },
|
||||
{ value: 'text/plain', text: this.$t('contentType.text_plain') },
|
||||
],
|
||||
checkboxLabel: {
|
||||
on: this.$t('general:label.general.yes'),
|
||||
off: this.$t('general:label.general.no'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fresh () {
|
||||
return !this.template.templateID || this.template.templateID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : true // this.template.canUpdateRole
|
||||
},
|
||||
|
||||
shortState () {
|
||||
return this.template.meta.short ? null : false
|
||||
},
|
||||
|
||||
handleState () {
|
||||
return handle.handleState(this.template.handle)
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.shortState, this.handleState].includes(false)
|
||||
},
|
||||
|
||||
getDeleteStatus () {
|
||||
return this.template.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
|
||||
getArchiveStatus () {
|
||||
return this.template.archivedAt ? this.$t('unarchive') : this.$t('archive')
|
||||
},
|
||||
|
||||
getDeletedButtonStatusCypressId () {
|
||||
return `button-${this.getDeleteStatus.toLowerCase()}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
52
client/web/admin/src/components/User/C3.js
Normal file
52
client/web/admin/src/components/User/C3.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { default as component } from './CUserEditorInfo.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
user: {
|
||||
userID: '',
|
||||
email: 'email@mail.bg',
|
||||
name: 'Stefan',
|
||||
handle: 'SS',
|
||||
},
|
||||
uiPage: 'user/editor',
|
||||
resourceType: 'system:user',
|
||||
uiSlot: 'infoFooter',
|
||||
processing: false,
|
||||
success: false,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Editor info',
|
||||
group: ['User'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('Processing', 'processing'),
|
||||
checkbox('Success', 'success'),
|
||||
checkbox('Enable delete and suspend', {
|
||||
value (p) { return p.user.userID.length > 0 },
|
||||
update (p, val) { p.user.userID = val ? '123456789' : '' },
|
||||
}),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
user: {
|
||||
...props.user,
|
||||
email: '',
|
||||
name: '',
|
||||
handle: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
170
client/web/admin/src/components/User/CUserEditorAvatar.vue
Normal file
170
client/web/admin/src/components/User/CUserEditorAvatar.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-user-profile-avatar"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
enctype="multipart/form-data"
|
||||
@submit.prevent="$emit('submit', user)"
|
||||
>
|
||||
<img
|
||||
:src="uploadedAvatar('avatar')"
|
||||
style="height: 4rem; width: 4rem;"
|
||||
class="rounded-circle mb-4"
|
||||
>
|
||||
|
||||
<div
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<c-uploader-with-preview
|
||||
:endpoint="$SystemAPI.userProfileAvatarEndpoint({ userID: user.userID })"
|
||||
@upload="$emit('onUpload')"
|
||||
@clear="$emit('resetAttachment', 'avatar')"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
v-if="uploadedAvatar('avatar') && isKindAvatar"
|
||||
:processing="processingAvatar"
|
||||
:text="$t('general:label.delete')"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
variant="danger"
|
||||
class="ml-2 h-100"
|
||||
@confirmed="$emit('resetAttachment', 'avatar')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<b-row class="mt-3">
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('initial.color')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-color-picker
|
||||
v-model="user.meta.avatarColor"
|
||||
data-test-id="input-text-color"
|
||||
:translations="{
|
||||
modalTitle: $t('colorPicker'),
|
||||
light: $t('ui.settings:editor.corteza-studio.tabs.light'),
|
||||
dark: $t('ui.settings:editor.corteza-studio.tabs.dark'),
|
||||
cancelBtnLabel: $t('general:label.cancel'),
|
||||
saveBtnLabel: $t('general:label.saveAndClose')
|
||||
}"
|
||||
:theme-settings="themeSettings"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('initial.backgroundColor')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-color-picker
|
||||
v-model="user.meta.avatarBgColor"
|
||||
data-test-id="input-background-color"
|
||||
:translations="{
|
||||
modalTitle: $t('colorPicker'),
|
||||
light: $t('ui.settings:editor.corteza-studio.tabs.light'),
|
||||
dark: $t('ui.settings:editor.corteza-studio.tabs.dark'),
|
||||
cancelBtnLabel: $t('general:label.cancel'),
|
||||
saveBtnLabel: $t('general:label.saveAndClose')
|
||||
}"
|
||||
:theme-settings="themeSettings"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', user)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CUploaderWithPreview from 'corteza-webapp-admin/src/components/CUploaderWithPreview'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputColorPicker } = components
|
||||
|
||||
export default {
|
||||
name: 'CUserEditorAvatar',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
keyPrefix: 'editor.avatar',
|
||||
},
|
||||
|
||||
components: {
|
||||
CUploaderWithPreview,
|
||||
CInputColorPicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
processingAvatar: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isKindAvatar () {
|
||||
return this.user.meta.avatarKind === 'avatar'
|
||||
},
|
||||
|
||||
themeSettings () {
|
||||
return this.$Settings.get('ui.studio.themes', [])
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
uploadedAvatar (name) {
|
||||
const attachmentID = this.user.meta.avatarID
|
||||
|
||||
if (attachmentID !== '0') {
|
||||
return (
|
||||
this.$SystemAPI.baseURL +
|
||||
this.$SystemAPI.attachmentOriginalEndpoint({
|
||||
attachmentID,
|
||||
kind: 'avatar',
|
||||
name,
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<b-card
|
||||
v-if="value && value.length > 0"
|
||||
data-test-id="card-external-auth-providers"
|
||||
header-class="border-bottom"
|
||||
body-class="p-0"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-table
|
||||
:items="value"
|
||||
:fields="fields"
|
||||
head-variant="light"
|
||||
responsive
|
||||
hover
|
||||
class="mb-0"
|
||||
style="min-height: 200px;"
|
||||
>
|
||||
<template #cell(editor)="{ item }">
|
||||
<c-input-confirm
|
||||
data-test-id="button-remove-provider"
|
||||
show-icon
|
||||
@confirmed="$emit('delete', item.credentialsID)"
|
||||
/>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CUserEditorExternalAuthProviders',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
keyPrefix: 'editor.external-auth-providers',
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
fields () {
|
||||
return [
|
||||
{ key: 'label', label: this.$t('label'), thStyle: { width: '350px' } },
|
||||
{ key: 'type', label: this.$t('type'), thStyle: { width: '250px' } },
|
||||
{ key: 'editor', label: '', tdClass: 'text-right' },
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
248
client/web/admin/src/components/User/CUserEditorInfo.vue
Normal file
248
client/web/admin/src/components/User/CUserEditorInfo.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-user-info"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', user)"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('email')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="user.email"
|
||||
data-test-id="input-email"
|
||||
required
|
||||
:state="emailState"
|
||||
type="email"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="user.name"
|
||||
data-test-id="input-name"
|
||||
required
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('handle')"
|
||||
:class="{ 'mb-0': !user.userID }"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="user.handle"
|
||||
data-test-id="input-handle"
|
||||
:placeholder="$t('placeholder-handle')"
|
||||
:state="handleState"
|
||||
/>
|
||||
<b-form-invalid-feedback :state="handleState">
|
||||
{{ $t('invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('userGroup.label')"
|
||||
:class="{ 'mb-0': !user.userID }"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-user-group
|
||||
v-model="user.userGroupID"
|
||||
:placeholder="$t('userGroup.placeholder')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:id="user.userID"
|
||||
:resource="user"
|
||||
/>
|
||||
|
||||
<!--
|
||||
include hidden input to enable
|
||||
trigger submit event w/ ENTER
|
||||
-->
|
||||
<input
|
||||
type="submit"
|
||||
class="d-none"
|
||||
:disabled="saveDisabled"
|
||||
>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="!fresh && user.canDeleteUser"
|
||||
:data-test-id="deletedButtonStatusCypressId"
|
||||
:text="getDeleteStatus"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
v-if="!fresh"
|
||||
:data-test-id="suspendButtonStatusCypressId"
|
||||
:text="getSuspendStatus"
|
||||
variant="light"
|
||||
size="md"
|
||||
@confirmed="$emit('status')"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
v-if="!fresh"
|
||||
data-test-id="button-sessions-revoke"
|
||||
:text="$t('revokeAllSession')"
|
||||
:disabled="user.userID === userID"
|
||||
variant="light"
|
||||
size="md"
|
||||
@confirmed="$emit('sessionsRevoke')"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
v-if="!fresh && !user.emailConfirmed"
|
||||
variant="light"
|
||||
@click="$emit('patch', '/emailConfirmed', true)"
|
||||
>
|
||||
{{ $t('confirmEmail') }}
|
||||
</b-button>
|
||||
|
||||
<c-corredor-manual-buttons
|
||||
ui-page="user/editor"
|
||||
ui-slot="infoFooter"
|
||||
resource-type="system:user"
|
||||
default-variant="light"
|
||||
@click="dispatchCortezaSystemUserEvent($event, { user })"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', user)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
import { handle, components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputUserGroup } = components
|
||||
|
||||
export default {
|
||||
name: 'CUserEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
components: {
|
||||
CInputUserGroup,
|
||||
},
|
||||
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
getDeleteStatus () {
|
||||
return this.user.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
|
||||
getSuspendStatus () {
|
||||
return this.user.suspendedAt ? this.$t('unsuspend') : this.$t('suspend')
|
||||
},
|
||||
|
||||
userID () {
|
||||
if (this.$auth.user) {
|
||||
return this.$auth.user.userID
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
|
||||
fresh () {
|
||||
return !this.user.userID || this.user.userID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : this.user.canUpdateUser
|
||||
},
|
||||
|
||||
emailState () {
|
||||
const { email } = this.user
|
||||
return email ? null : false
|
||||
},
|
||||
|
||||
handleState () {
|
||||
return handle.handleState(this.user.handle)
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.emailState, this.handleState].includes(false)
|
||||
},
|
||||
|
||||
deletedButtonStatusCypressId () {
|
||||
return `button-${this.getDeleteStatus.toLowerCase()}`
|
||||
},
|
||||
|
||||
suspendButtonStatusCypressId () {
|
||||
return `button-${this.getSuspendStatus.toLowerCase()}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
97
client/web/admin/src/components/User/CUserEditorMFA.vue
Normal file
97
client/web/admin/src/components/User/CUserEditorMFA.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="border-bottom"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<div>
|
||||
<span
|
||||
v-if="mfa.enforcedEmailOTP"
|
||||
v-html="$t('emailOTP.enabled.text')"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
v-html="$t('emailOTP.disabled.text')"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<b-button
|
||||
v-if="mfa.enforcedEmailOTP"
|
||||
@click="$emit('patch', '/meta/securityPolicy/mfa/enforcedEmailOTP', false)"
|
||||
>
|
||||
{{ $t('emailOTP.disable.label') }}
|
||||
</b-button>
|
||||
<b-button
|
||||
v-else
|
||||
@click="$emit('patch', '/meta/securityPolicy/mfa/enforcedEmailOTP', true)"
|
||||
>
|
||||
{{ $t('emailOTP.enable.label') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap mt-2 pt-2 border-top">
|
||||
<div>
|
||||
<span
|
||||
v-if="mfa.enforcedTOTP"
|
||||
v-html="$t('TOTP.enabled.text')"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
v-html="$t('TOTP.disabled.text')"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<b-button
|
||||
:disabled="!mfa.enforcedTOTP"
|
||||
@click="$emit('patch', '/meta/securityPolicy/mfa/enforcedTOTP', false)"
|
||||
>
|
||||
{{ $t('TOTP.remove.label') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* @todo find a way to get this number from the backend
|
||||
* @type {number}
|
||||
*/
|
||||
// const minPasswordLength = 5
|
||||
|
||||
export default {
|
||||
name: 'CUserEditorMFA',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
keyPrefix: 'editor.mfa',
|
||||
},
|
||||
|
||||
props: {
|
||||
mfa: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
168
client/web/admin/src/components/User/CUserEditorPassword.vue
Normal file
168
client/web/admin/src/components/User/CUserEditorPassword.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-user-password"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h4
|
||||
data-test-id="card-title"
|
||||
class="m-0"
|
||||
>
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
@submit.prevent="onPasswordSubmit"
|
||||
>
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
:label="$t('new')"
|
||||
:description="getPasswordWarning"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="password"
|
||||
data-test-id="input-new-password"
|
||||
:state="passwordState"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col cols="12">
|
||||
<b-form-group
|
||||
:label="$t('confirm')"
|
||||
:description="getConfirmPasswordWarning"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="confirmPassword"
|
||||
data-test-id="input-confirm-password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
:disabled="!passwordState"
|
||||
:state="confirmPasswordState"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
data-test-id="button-remove-password"
|
||||
:text="$t('removePassword')"
|
||||
variant="light"
|
||||
size="md"
|
||||
@confirmed="$emit('submit')"
|
||||
/>
|
||||
|
||||
<c-corredor-manual-buttons
|
||||
ui-page="user/editor"
|
||||
ui-slot="passwordFooter"
|
||||
resource-type="system:user"
|
||||
default-variant="light"
|
||||
@click="dispatchCortezaSystemEvent($event)"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="!passwordState || !confirmPasswordState"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="onPasswordSubmit"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CUserEditorPassword',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
keyPrefix: 'editor.password',
|
||||
},
|
||||
|
||||
props: {
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
userID: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
minPasswordLength: this.$Settings.get('auth.internal.passwordConstraints.minLength', 8),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
passwordState () {
|
||||
if (this.password.length > 0) {
|
||||
return this.password.length >= this.minPasswordLength
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
confirmPasswordState () {
|
||||
if (this.passwordState && this.confirmPassword.length > 0) {
|
||||
return this.password === this.confirmPassword
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
getPasswordWarning () {
|
||||
if (this.passwordState === false) {
|
||||
return this.$t('length', { length: this.minPasswordLength })
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
getConfirmPasswordWarning () {
|
||||
if (this.confirmPasswordState === false) {
|
||||
return this.$t('missmatch')
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onPasswordSubmit () {
|
||||
this.$emit('submit', this.password)
|
||||
|
||||
this.password = ''
|
||||
this.confirmPassword = ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
66
client/web/admin/src/components/User/CUserEditorRoles.vue
Normal file
66
client/web/admin/src/components/User/CUserEditorRoles.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-role-membership"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit')"
|
||||
>
|
||||
<c-role-picker
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CRolePicker from 'corteza-webapp-admin/src/components/CRolePicker'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
keyPrefix: 'editor.roles',
|
||||
},
|
||||
|
||||
components: {
|
||||
CRolePicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="inclRoleMembership"
|
||||
data-test-id="checkbox-include-role-membership"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
>
|
||||
{{ $t('export.inclRoleMembership') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:description="!inclRoleMembership
|
||||
? $t('export.membershipRequiredLabel')
|
||||
: ''
|
||||
"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="inclRoles"
|
||||
data-test-id="checkbox-include-roles"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
:disabled="!inclRoleMembership"
|
||||
>
|
||||
{{ $t('export.inclRoles') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<div slot="footer">
|
||||
<b-button
|
||||
data-test-id="button-export"
|
||||
variant="primary"
|
||||
class="float-right"
|
||||
@click="nextStep"
|
||||
>
|
||||
{{ $t('export.export') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
},
|
||||
|
||||
name: 'CUserExportConfiguration',
|
||||
|
||||
data () {
|
||||
return {
|
||||
inclRoleMembership: false,
|
||||
inclRoles: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
nextStep () {
|
||||
const rtr = {
|
||||
inclRoleMembership: this.inclRoleMembership,
|
||||
inclRoles: this.inclRoles,
|
||||
}
|
||||
|
||||
this.$emit('configured', rtr)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="d-flex">
|
||||
<b-button
|
||||
data-test-id="button-export"
|
||||
variant="light"
|
||||
size="lg"
|
||||
class="flex-fill"
|
||||
@click="showModal=true"
|
||||
>
|
||||
{{ $t('export.buttonLabel') }}
|
||||
</b-button>
|
||||
|
||||
<b-modal
|
||||
:visible="showModal"
|
||||
size="lg"
|
||||
:title="$t('export.title')"
|
||||
hide-footer
|
||||
centered
|
||||
@hide="onModalHide"
|
||||
>
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="stepComponent"
|
||||
v-if="!processing"
|
||||
v-bind="$props"
|
||||
:session="session"
|
||||
@configured="onConfigured"
|
||||
@close="onClose"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="p-5 h-100 d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
</keep-alive>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ExportConfiguration from './ExportConfiguration.vue'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
},
|
||||
|
||||
name: 'CUserExport',
|
||||
|
||||
data () {
|
||||
return {
|
||||
step: 0,
|
||||
showModal: false,
|
||||
session: {},
|
||||
components: [ExportConfiguration],
|
||||
|
||||
processing: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
stepComponent () {
|
||||
return this.components[this.step]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onModalHide () {
|
||||
this.step = 0
|
||||
this.session = {}
|
||||
this.showModal = false
|
||||
},
|
||||
|
||||
async onConfigured (e) {
|
||||
this.processing = true
|
||||
this.$emit('export', e)
|
||||
this.onReset()
|
||||
this.onClose()
|
||||
this.processing = false
|
||||
},
|
||||
|
||||
onReset () {
|
||||
this.step = 0
|
||||
this.$emit('reset')
|
||||
},
|
||||
|
||||
onClose () {
|
||||
this.showModal = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
<c-uploader
|
||||
:labels="{
|
||||
placeholder: $t('general:label.dropFiles'),
|
||||
uploading: $t('general:label.uploading'),
|
||||
fileTypeNotAllowed: $t('general:label.fileTypeNotAllowed'),
|
||||
}"
|
||||
:endpoint="userImportEndpoint"
|
||||
:accepted-files="['application/zip']"
|
||||
@upload="onUploaded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CUploader } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
},
|
||||
|
||||
name: 'CFileUpload',
|
||||
|
||||
components: {
|
||||
CUploader,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
session: null,
|
||||
sessionFile: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
userImportEndpoint () {
|
||||
return this.$SystemAPI.baseURL + this.$SystemAPI.userImportEndpoint({})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onUploaded () {
|
||||
this.$emit('imported', {})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="d-flex">
|
||||
<b-button
|
||||
data-test-id="button-import"
|
||||
variant="light"
|
||||
size="lg"
|
||||
class="flex-fill"
|
||||
@click="showModal=true"
|
||||
>
|
||||
{{ $t('import.buttonLabel') }}
|
||||
</b-button>
|
||||
|
||||
<b-modal
|
||||
:visible="showModal"
|
||||
size="lg"
|
||||
:title="$t('import.title')"
|
||||
centered
|
||||
hide-footer
|
||||
@hide="onModalHide"
|
||||
>
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="stepComponent"
|
||||
v-bind="$props"
|
||||
@imported="onImported"
|
||||
@reset="onReset"
|
||||
@close="onClose"
|
||||
/>
|
||||
</keep-alive>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileUpload from './FileUpload'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.users',
|
||||
},
|
||||
|
||||
name: 'CUserImport',
|
||||
|
||||
data () {
|
||||
return {
|
||||
step: 0,
|
||||
showModal: false,
|
||||
components: [FileUpload],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
stepComponent () {
|
||||
return this.components[this.step]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onModalHide () {
|
||||
this.step = 0
|
||||
this.showModal = false
|
||||
},
|
||||
|
||||
onImported (e) {
|
||||
this.$emit('imported')
|
||||
this.onReset()
|
||||
this.onClose()
|
||||
},
|
||||
|
||||
onReset () {
|
||||
this.step = 0
|
||||
this.$emit('reset')
|
||||
},
|
||||
|
||||
onClose () {
|
||||
this.showModal = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-user-group-info"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit', userGroup)"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('meta.short')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="userGroup.meta.short"
|
||||
data-test-id="input-name"
|
||||
required
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('handle')"
|
||||
:class="{ 'mb-0': !userGroup.userGroupID }"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="userGroup.handle"
|
||||
data-test-id="input-handle"
|
||||
:placeholder="$t('placeholder-handle')"
|
||||
:state="handleState"
|
||||
/>
|
||||
<b-form-invalid-feedback :state="handleState">
|
||||
{{ $t('invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('meta.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="userGroup.meta.description"
|
||||
data-test-id="textarea-description"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<c-system-fields
|
||||
:id="userGroup.userGroupID"
|
||||
:resource="userGroup"
|
||||
/>
|
||||
|
||||
<template v-if="!isRoot">
|
||||
<hr>
|
||||
|
||||
<div v-if="!isRoot">
|
||||
<h5 class="mb-3">
|
||||
{{ $t('parents.title') }}
|
||||
</h5>
|
||||
|
||||
<c-form-table-wrapper
|
||||
:labels="{
|
||||
addButton: $t('general:label.add')
|
||||
}"
|
||||
class="my-3"
|
||||
@add-item="addParent"
|
||||
>
|
||||
<b-table-simple
|
||||
v-if="userGroup.config.path"
|
||||
borderless
|
||||
small
|
||||
responsive
|
||||
>
|
||||
<b-thead>
|
||||
<b-tr>
|
||||
<b-th class="text-primary">
|
||||
{{ $t('parents.parent.label') }}
|
||||
</b-th>
|
||||
<b-th class="text-primary">
|
||||
{{ $t('parents.name.label') }}
|
||||
</b-th>
|
||||
<b-th v-if="userGroup.config.path.length > 1" />
|
||||
</b-tr>
|
||||
</b-thead>
|
||||
<b-tbody>
|
||||
<b-tr
|
||||
v-for="(parent, i) in userGroup.config.path"
|
||||
:key="i"
|
||||
>
|
||||
<b-td
|
||||
class="align-middle"
|
||||
style="min-width: 250px;"
|
||||
>
|
||||
<c-input-user-group
|
||||
v-model="parent.selfID"
|
||||
:placeholder="$t('parents.parent.placeholder')"
|
||||
/>
|
||||
</b-td>
|
||||
<b-td
|
||||
class="align-middle"
|
||||
style="min-width: 200px;"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="parent.name"
|
||||
:placeholder="$t('parents.name.placeholder')"
|
||||
/>
|
||||
</b-td>
|
||||
<b-td
|
||||
v-if="userGroup.config.path.length > 1"
|
||||
class="text-right align-middle"
|
||||
style="width: 1%"
|
||||
>
|
||||
<c-input-confirm
|
||||
show-icon
|
||||
@confirmed="deleteParent(i)"
|
||||
/>
|
||||
</b-td>
|
||||
</b-tr>
|
||||
</b-tbody>
|
||||
</b-table-simple>
|
||||
</c-form-table-wrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
include hidden input to enable
|
||||
trigger submit event w/ ENTER
|
||||
-->
|
||||
<input
|
||||
type="submit"
|
||||
class="d-none"
|
||||
:disabled="saveDisabled"
|
||||
>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-input-confirm
|
||||
v-if="!fresh && userGroup.canDeleteUserGroup"
|
||||
:data-test-id="deletedButtonStatusCypressId"
|
||||
:text="getDeleteStatus"
|
||||
variant="danger"
|
||||
size="md"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<c-corredor-manual-buttons
|
||||
ui-page="user-group/editor"
|
||||
ui-slot="infoFooter"
|
||||
resource-type="system:user-group"
|
||||
default-variant="light"
|
||||
@click="dispatchCortezaSystemUserGroupEvent($event, { userGroup })"
|
||||
/>
|
||||
|
||||
<c-button-submit
|
||||
:disabled="saveDisabled"
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit', userGroup)"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NoID } from '@cortezaproject/corteza-js'
|
||||
import { handle, components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputUserGroup } = components
|
||||
|
||||
export default {
|
||||
name: 'CUserGroupEditorInfo',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.user-groups',
|
||||
keyPrefix: 'editor.info',
|
||||
},
|
||||
|
||||
components: {
|
||||
CInputUserGroup,
|
||||
},
|
||||
|
||||
props: {
|
||||
userGroup: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isRoot () {
|
||||
return this.userGroup.isRoot
|
||||
},
|
||||
|
||||
getDeleteStatus () {
|
||||
return this.userGroup.deletedAt ? this.$t('undelete') : this.$t('delete')
|
||||
},
|
||||
|
||||
userGroupID () {
|
||||
if (this.userGroup) {
|
||||
return this.userGroup.userGroupID
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
|
||||
fresh () {
|
||||
return !this.userGroupID || this.userGroupID === NoID
|
||||
},
|
||||
|
||||
editable () {
|
||||
return this.fresh ? this.canCreate : this.userGroup.canUpdateUserGroup
|
||||
},
|
||||
|
||||
nameState () {
|
||||
return this.userGroup.meta.short ? null : false
|
||||
},
|
||||
|
||||
handleState () {
|
||||
return handle.handleState(this.userGroup.handle)
|
||||
},
|
||||
|
||||
saveDisabled () {
|
||||
return !this.editable || [this.nameState, this.handleState, this.parentState].includes(false)
|
||||
},
|
||||
|
||||
parentState () {
|
||||
if (this.isRoot) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!this.userGroup.config.path || this.userGroup.config.path.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.userGroup.config.path.every(parent => parent.selfID) ? null : false
|
||||
},
|
||||
|
||||
deletedButtonStatusCypressId () {
|
||||
return `button-${this.getDeleteStatus.toLowerCase()}`
|
||||
},
|
||||
|
||||
suspendButtonStatusCypressId () {
|
||||
return `button-${this.getSuspendStatus.toLowerCase()}`
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
userGroupID: {
|
||||
immediate: true,
|
||||
handler () {
|
||||
const { config = {} } = this.userGroup || {}
|
||||
const { path = [] } = config || {}
|
||||
|
||||
if (!this.isRoot && path.length === 0) {
|
||||
this.addParent()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addParent () {
|
||||
this.userGroup.config.path.push({
|
||||
selfID: '',
|
||||
name: '',
|
||||
})
|
||||
},
|
||||
|
||||
deleteParent (i) {
|
||||
this.userGroup.config.path.splice(i, 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-user-group-edit-members"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form @submit.prevent="$emit('submit')">
|
||||
<c-member-picker
|
||||
:value="value"
|
||||
no-remove
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t("title") }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CMemberPicker from 'corteza-webapp-admin/src/components/CMemberPicker'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.user-groups',
|
||||
keyPrefix: 'editor.members',
|
||||
},
|
||||
|
||||
components: {
|
||||
CMemberPicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<b-card
|
||||
data-test-id="card-user-group-edit-roles"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="$emit('submit')"
|
||||
>
|
||||
<c-role-picker
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
</b-form>
|
||||
|
||||
<template #header>
|
||||
<h4 class="m-0">
|
||||
{{ $t('title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:processing="processing"
|
||||
:success="success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CRolePicker from 'corteza-webapp-admin/src/components/CRolePicker'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'system.user-groups',
|
||||
keyPrefix: 'editor.roles',
|
||||
},
|
||||
|
||||
components: {
|
||||
CRolePicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
success: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user