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:
19
.editorconfig
Normal file
19
.editorconfig
Normal file
@@ -0,0 +1,19 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
indent_style = space
|
||||
|
||||
[*.cue]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
7
.eslintignore
Normal file
7
.eslintignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
api-clients/automation.ts
|
||||
api-clients/compose.ts
|
||||
api-clients/federation.ts
|
||||
api-clients/system.ts
|
||||
**/*.test.ts
|
||||
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
server/**/*gen.go linguist-generated=true
|
||||
server/**/*gen_test.go linguist-generated=true
|
||||
|
||||
server/vendor/** linguist-vendored=true
|
||||
server/go.mod linguist-vendored=true
|
||||
server/go.sum linguist-vendored=true
|
||||
|
||||
server/docs/*.yaml linguist-documentation=true
|
||||
server/docs/README.adoc linguist-documentation=true
|
||||
server/docs/index.html linguist-documentation=true
|
||||
103
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
103
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: Bug 🐞
|
||||
description: File a bug/issue with Corteza
|
||||
title: "When I try to do X I get Y"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
If this is your first time, please read the following:
|
||||
|
||||
Take a quick look on how Github's [markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) works. It will help you format your message and make it joy to read.
|
||||
|
||||
Corteza is a complex application with many moving parts. You can help us a lot
|
||||
by respecting the following instructions for bug reports.
|
||||
|
||||
# Web application bugs
|
||||
|
||||
When issue requires a certain steps to be followed, record a video, otherwise make a screenshot. If possible resize your window to a minimum size where application is still usable or make a screenshot or recording that focuses on a problem if possible and do not capture your whole screen. This keeps the video files smaller with contents, especially
|
||||
errors, values readable.
|
||||
|
||||
## Favours and versions of your operating system and browsers
|
||||
|
||||
Corteza is thoroughly tested on recent versions of Chrome and Firefox. You might have some special combination that we did not cover. If possible, upgrade your system and browser first. It is usually a good thing to do. If you are not able to or the problem persists, please share the version details.
|
||||
|
||||
# Server errors
|
||||
|
||||
Sometimes the error happens between the request your browser sends and the response it gets. In such cases we would love to see all headers and parameters of that HTTP request and how the response looks like. Text form is perfect.
|
||||
|
||||
If you have full control over your server, try enabling full debug logging in your .env fil by setting `LOG_DEBUG=true` and `LOG_LEVEL=debug`, restarting your server and sharing relevant log lines.
|
||||
|
||||
As for logs: we usually just need the lines that are related to the request. But if your server does not start or crashes right after, make sure you share everything from first log line on. There might be some values in there you want to obfuscate before sharing.
|
||||
|
||||
## Large responses or huge log files
|
||||
|
||||
If you want to share something that exceeds 42 lines, try [Github Gist](https://gist.github.com/) and just add a link to Gist you created.
|
||||
|
||||
## Favours and versions of your operating system, Docker and Docker Compose
|
||||
|
||||
A big share of issues is unrelated to any of these. You can skip this when preparing your bug report. If you did some changes and experimenting with Corteza in such a low level let us know what version you need.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version of Corteza
|
||||
placeholder: 202Y.X.x
|
||||
description: "You can make sure by visiting `/version` (e.g.: http://localhost:3000/version)"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment and versions
|
||||
description: |
|
||||
When it is clear that the problem is related to a browser and is not something that happens on the server (between request and response), share your operating system you are using and browser with versions for each one of them.
|
||||
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking on this area to highlight it and then by dragging files in it.
|
||||
validations:
|
||||
required: false
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Community Support
|
||||
url: https://forum.cortezaproject.org/
|
||||
about: Please ask and answer questions here.
|
||||
- name: Ideas, enhancements and feature requests 🚀
|
||||
url: https://github.com/orgs/cortezaproject/discussions
|
||||
about: Please open a new discussion and describe the idea you have.
|
||||
13
.github/PULL_REQUEST_TEMPLATE
vendored
Normal file
13
.github/PULL_REQUEST_TEMPLATE
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# The following changes are implemented
|
||||
TODO: Summary
|
||||
|
||||
# Changes in the user interface:
|
||||
TODO: Add screenshots, recordings or remove this section
|
||||
|
||||
# Checklist when submitting a final (!draft) PR
|
||||
- [ ] Commits are tidied up, squashed if needed and follow guidelines in CONTRIBUTING.md
|
||||
- [ ] Code builds
|
||||
- [ ] All existing tests pass
|
||||
- [ ] All new critical code is covered by tests
|
||||
- [ ] PR is linked to the relevant issue(s)
|
||||
- [ ] Rebased with the target branch
|
||||
1
.github/assets/corteza_logo.svg
vendored
Normal file
1
.github/assets/corteza_logo.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 241.45 38.63"><defs><style>.cls-1{fill:#FF9661;}</style></defs><path class="cls-1" d="M2.51,9.38A18,18,0,0,1,9.37,2.51,19.49,19.49,0,0,1,19.13.05,19.15,19.15,0,0,1,30.21,3.3a17,17,0,0,1,6.64,9h-8.7A9,9,0,0,0,24.5,8.21a10.67,10.67,0,0,0-5.43-1.35A11.24,11.24,0,0,0,13.21,8.4a10.6,10.6,0,0,0-4,4.35,14.41,14.41,0,0,0-1.43,6.59,14.44,14.44,0,0,0,1.43,6.57,10.55,10.55,0,0,0,4,4.38,11.24,11.24,0,0,0,5.86,1.54,10.49,10.49,0,0,0,5.43-1.38,9.16,9.16,0,0,0,3.65-4.08h8.7a16.92,16.92,0,0,1-6.62,9,19.27,19.27,0,0,1-11.1,3.21,19.49,19.49,0,0,1-9.76-2.46,18,18,0,0,1-6.86-6.83A19.69,19.69,0,0,1,0,19.34,19.76,19.76,0,0,1,2.51,9.38Z"/><path class="cls-1" d="M50.43,36.15a18.53,18.53,0,0,1-7-6.89,19.28,19.28,0,0,1-2.59-10A19.13,19.13,0,0,1,43.4,9.38a18.46,18.46,0,0,1,7-6.89,20.29,20.29,0,0,1,19.48,0,18.37,18.37,0,0,1,7,6.89,19.31,19.31,0,0,1,2.56,9.91,19.45,19.45,0,0,1-2.56,10,18.38,18.38,0,0,1-7,6.89,20.31,20.31,0,0,1-19.45,0Zm15.72-5.81a10.53,10.53,0,0,0,4-4.4,14.38,14.38,0,0,0,1.46-6.65,14.2,14.2,0,0,0-1.46-6.62,10.4,10.4,0,0,0-4-4.35,12.72,12.72,0,0,0-12,0,10.34,10.34,0,0,0-4.08,4.35,14.32,14.32,0,0,0-1.46,6.62,14.51,14.51,0,0,0,1.46,6.65,10.47,10.47,0,0,0,4.08,4.4,12.5,12.5,0,0,0,12,0Z"/><path class="cls-1" d="M103.92,38.26,95.6,23.56H92v14.7H84.47V.54H98.62a16.49,16.49,0,0,1,7.46,1.54,10.8,10.8,0,0,1,4.62,4.16,11.32,11.32,0,0,1,1.54,5.86,11.23,11.23,0,0,1-2.16,6.73A11,11,0,0,1,103.65,23l9,15.3ZM92,17.89h6.32A6.33,6.33,0,0,0,103,16.4a5.52,5.52,0,0,0,1.51-4.13,5.29,5.29,0,0,0-1.51-4,6.5,6.5,0,0,0-4.6-1.43H92Z"/><path class="cls-1" d="M142.88.54V6.65h-10V38.26h-7.57V6.65H115.21V.54Z"/><path class="cls-1" d="M155.32,6.65V16.1H168v6h-12.7v10h14.32v6.16H147.75V.49h21.89V6.65Z"/><path class="cls-1" d="M185.42,31.88h17v6.38H176.66V32.42l16.86-25.5H176.66V.54h25.72V6.38Z"/><path class="cls-1" d="M231,31.07H216l-2.49,7.19h-7.94L219.08.49h8.81l13.56,37.77h-8ZM228.91,25,223.46,9.24,218,25Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
4
.github/workflows/assets/client/config.js
vendored
Normal file
4
.github/workflows/assets/client/config.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// Corteza API location
|
||||
window.CortezaAPI = '//localhost:8888/api'
|
||||
|
||||
window.i18nPseudoModeEnabled = false
|
||||
242
.github/workflows/assets/client/vue.config-builder.js
vendored
Normal file
242
.github/workflows/assets/client/vue.config-builder.js
vendored
Normal file
@@ -0,0 +1,242 @@
|
||||
const webpack = require('webpack')
|
||||
const exec = require('child_process').execSync
|
||||
const path = require('path')
|
||||
const Vue = require('vue')
|
||||
|
||||
module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, theme, packageAlias, root = path.resolve('.'), env = process.env.NODE_ENV }) => {
|
||||
const isDevelopment = (env === 'development')
|
||||
const isTest = (env === 'test')
|
||||
|
||||
if (isTest) {
|
||||
Vue.config.devtools = false
|
||||
Vue.config.productionTip = false
|
||||
}
|
||||
|
||||
if (isDevelopment) {
|
||||
Vue.config.devtools = true
|
||||
Vue.config.performance = true
|
||||
}
|
||||
|
||||
const optimization = isTest
|
||||
? {}
|
||||
: {
|
||||
usedExports: true,
|
||||
runtimeChunk: 'single',
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 20000,
|
||||
maxSize: 244000,
|
||||
cacheGroups: {
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
chunks: 'all',
|
||||
priority: -10,
|
||||
},
|
||||
common: {
|
||||
minChunks: 2,
|
||||
priority: -20,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
publicPath: isDevelopment ? '/' : './',
|
||||
lintOnSave: true,
|
||||
runtimeCompiler: true,
|
||||
|
||||
configureWebpack: {
|
||||
// other webpack options to merge in ...
|
||||
|
||||
// Webpack 5 specific configuration
|
||||
resolve: {
|
||||
symlinks: false,
|
||||
fallback: {
|
||||
path: false,
|
||||
fs: false, // Add explicit false for Node.js core modules
|
||||
crypto: false,
|
||||
stream: false,
|
||||
util: false,
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
FLAVOUR: JSON.stringify(appFlavour),
|
||||
WEBAPP: JSON.stringify(appLabel),
|
||||
VERSION: JSON.stringify(version || ('' + exec('git describe --always --tags')).trim()),
|
||||
BUILD_TIME: JSON.stringify((new Date()).toISOString()),
|
||||
}),
|
||||
],
|
||||
|
||||
optimization: {
|
||||
...optimization,
|
||||
moduleIds: 'deterministic',
|
||||
chunkIds: 'named',
|
||||
emitOnErrors: false,
|
||||
},
|
||||
|
||||
stats: {
|
||||
warnings: false,
|
||||
errors: false,
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: isDevelopment ? false : 'warning',
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000,
|
||||
},
|
||||
},
|
||||
|
||||
chainWebpack: config => {
|
||||
// https://cli.vuejs.org/guide/troubleshooting.html#symbolic-links-in-node-modules
|
||||
config.resolve.symlinks(false)
|
||||
|
||||
// Remove css extraction issues
|
||||
// https://github.com/vuejs/vue-cli/issues/3771#issuecomment-526228100
|
||||
config.plugin('friendly-errors').tap(args => {
|
||||
const vueCli3Transformer = args[0].additionalTransformers[0]
|
||||
args[0].additionalTransformers = [
|
||||
vueCli3Transformer,
|
||||
error => {
|
||||
return /\[mini-css-extract-plugin\]/.test(error.message) ? {} : error
|
||||
},
|
||||
]
|
||||
return args
|
||||
})
|
||||
|
||||
// Update copy-webpack-plugin configuration for v8+
|
||||
config.plugins.has('copy') && config.plugin('copy').tap(options => {
|
||||
options[0].patterns = [{
|
||||
from: 'public',
|
||||
globOptions: {
|
||||
ignore: ['**/config*js', '**/*gitignore', '**/index.html'],
|
||||
},
|
||||
noErrorOnMissing: true,
|
||||
}]
|
||||
return options
|
||||
})
|
||||
|
||||
// Aliasing full package name instead of '@' so we do
|
||||
// not break imports on apps that import this code
|
||||
config.resolve.alias.delete('@')
|
||||
if (packageAlias) {
|
||||
config.resolve.alias.set(packageAlias, root)
|
||||
}
|
||||
|
||||
if (isTest) {
|
||||
const scssRule = config.module.rule('scss')
|
||||
scssRule.uses.clear()
|
||||
scssRule
|
||||
.use('null-loader')
|
||||
.loader('null-loader')
|
||||
}
|
||||
|
||||
const scssNormal = config.module.rule('scss').oneOf('normal')
|
||||
|
||||
scssNormal.use('sass-loader')
|
||||
.loader('sass-loader')
|
||||
.tap(options => ({
|
||||
...options,
|
||||
sourceMap: true,
|
||||
sassOptions: {
|
||||
outputStyle: isDevelopment ? 'expanded' : 'compressed',
|
||||
},
|
||||
}))
|
||||
|
||||
// Load CSS assets according to their location
|
||||
scssNormal.use('resolve-url-loader')
|
||||
.loader('resolve-url-loader').options({
|
||||
keepQuery: true,
|
||||
removeCR: true,
|
||||
root: path.join(root, 'src/themes', theme),
|
||||
})
|
||||
.before('sass-loader')
|
||||
|
||||
// Keep this to ensure we don't have multiple HTML plugins
|
||||
},
|
||||
|
||||
devServer: {
|
||||
host: '127.0.0.1',
|
||||
port: 8080,
|
||||
hot: true,
|
||||
allowedHosts: 'all', // replaces disableHostCheck
|
||||
webSocketServer: 'ws',
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
|
||||
proxy: {
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
// Webpack 5 DevServer configuration
|
||||
watchFiles: {
|
||||
paths: [
|
||||
'**/*',
|
||||
'!**/node_modules/!(@cortezaproject)/**',
|
||||
],
|
||||
options: {
|
||||
usePolling: true,
|
||||
aggregateTimeout: 200,
|
||||
poll: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
client: {
|
||||
overlay: false,
|
||||
progress: true,
|
||||
},
|
||||
},
|
||||
|
||||
css: {
|
||||
sourceMap: isDevelopment,
|
||||
extract: !isTest,
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
sassOptions: {
|
||||
outputStyle: isDevelopment ? 'expanded' : 'compressed',
|
||||
},
|
||||
},
|
||||
postcss: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
require('autoprefixer'),
|
||||
require('rtlcss'),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function fetchBaseUrl () {
|
||||
const fs = require('fs')
|
||||
const window = {}
|
||||
|
||||
const fileContents =
|
||||
fs.existsSync('public/config.js')
|
||||
? fs.readFileSync('public/config.js', 'utf-8')
|
||||
: ''
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(fileContents)
|
||||
|
||||
const u = window.CortezaAPI || ''
|
||||
const ur = new URL(u.startsWith('//') ? `http:${u}` : u)
|
||||
|
||||
return `${ur.protocol}//${ur.host}/`
|
||||
} catch (e) {
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
62
.github/workflows/assets/docker-compose.yaml
vendored
Normal file
62
.github/workflows/assets/docker-compose.yaml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
backend:
|
||||
network_mode: host
|
||||
image: golang:${GO_VERSION}
|
||||
volumes:
|
||||
- "${PWD}:/corteza"
|
||||
entrypoint: |
|
||||
sh -c "
|
||||
git config --global --add safe.directory /corteza &&
|
||||
go run /corteza/server/cmd/corteza/main.go serve-api
|
||||
"
|
||||
working_dir: /corteza/server
|
||||
environment:
|
||||
- DB_DSN=postgres://corteza:root@localhost:5432/corteza_cy_test?sslmode=disable
|
||||
- HTTP_ADDR=:8888
|
||||
- HTTP_WEBAPP_ENABLED=true
|
||||
- ENVIRONMENT=dev
|
||||
- AUTH_REQUEST_RATE_LIMIT=0
|
||||
- FEDERATION_ENABLED=true
|
||||
- FEDERATION_HOST=http://corteza
|
||||
- FEDERATION_LABEL=Corteza e2e federation host
|
||||
healthcheck:
|
||||
test: curl --silent --fail-early --fail http://localhost:8888/healthcheck || exit 1
|
||||
interval: 30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
postgres:
|
||||
network_mode: host
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_USER: corteza
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: corteza_cy_test
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
client:
|
||||
network_mode: host
|
||||
image: node:22-slim
|
||||
volumes:
|
||||
- "${PWD}:/corteza"
|
||||
working_dir: /corteza
|
||||
environment:
|
||||
BUILD_VERSION: 2024.9.10
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 --spider http://localhost:8080 || exit 1
|
||||
interval: 30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 2g
|
||||
517
.github/workflows/release.yml
vendored
Normal file
517
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,517 @@
|
||||
name: "Release"
|
||||
|
||||
# These jobs can be tested with nektos/act tool
|
||||
# https://github.com/nektos/act
|
||||
#
|
||||
# Look for "!env.ACT" in the DRY_RUN expression below
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
BUILD_OS: linux
|
||||
BUILD_ARCH: amd64
|
||||
|
||||
GO_VERSION: 1.24.1
|
||||
GOFLAGS: -mod=readonly
|
||||
|
||||
NODE_VERSION: 22
|
||||
|
||||
RELEASE_BASE_URL: "https://releases.cortezaproject.org/files"
|
||||
SASS_VERSION: 1.85.1
|
||||
|
||||
jobs:
|
||||
server-test:
|
||||
name: "Server tests"
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
GOFLAGS: -mod=readonly
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with: { go-version: "${{ env.GO_VERSION }}" }
|
||||
- name: "Copy language files"
|
||||
working-directory: server/pkg/locale
|
||||
run: make src/en
|
||||
- name: "Unit"
|
||||
working-directory: server
|
||||
run: make test.unit
|
||||
- name: "Store"
|
||||
working-directory: server
|
||||
run: make test.store
|
||||
- name: "Integration"
|
||||
working-directory: server
|
||||
run: make test.integration
|
||||
|
||||
# Building web console for server and cache it for release-* steps
|
||||
# we'll do this in parallel with tests to gain a few seconds
|
||||
server-web-console-build:
|
||||
name: "Server Web Console Build"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with: { node-version: "${{ env.NODE_VERSION }}" }
|
||||
- uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-node-
|
||||
- name: "Setup YARN"
|
||||
run: npm install -g yarn @vue/cli-service
|
||||
- name: "Install dependencies"
|
||||
working-directory: server/webconsole
|
||||
run: yarn install
|
||||
- name: "Build Package"
|
||||
working-directory: server/webconsole
|
||||
run: yarn build
|
||||
- name: "Cache console build"
|
||||
uses: actions/cache@v3
|
||||
with: { path: ./server/webconsole/dist, key: console-build }
|
||||
|
||||
server-release:
|
||||
name: "Server Release (${{ matrix.os }})"
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- { runsOn: "ubuntu-22.04", os: "linux" }
|
||||
- { runsOn: "macos-latest", os: "darwin" }
|
||||
runs-on: ${{ matrix.runsOn }}
|
||||
needs: [ server-web-console-build, server-test ]
|
||||
env:
|
||||
BUILD_OS: ${{ matrix.os }}
|
||||
RELEASE_SFTP_KEY: ${{ secrets.RELEASE_SFTP_KEY }}
|
||||
RELEASE_SFTP_URI: ${{ secrets.RELEASE_SFTP_URI }}
|
||||
steps:
|
||||
- run: echo "BUILD_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with: { go-version: "${{ env.GO_VERSION }}" }
|
||||
- name: "Restore web console dist from cache"
|
||||
uses: actions/cache@v3
|
||||
with: { path: ./server/webconsole/dist, key: console-build }
|
||||
- name: "Copy essentials"
|
||||
run: cp *.md DCO LICENSE server/
|
||||
- name: "Copy language files"
|
||||
working-directory: server/pkg/locale
|
||||
run: make src/en src/de src/fr
|
||||
- name: "Build"
|
||||
working-directory: server
|
||||
run: make release-clean release
|
||||
- name: "Upload"
|
||||
working-directory: server
|
||||
run: make upload
|
||||
if: ${{ !env.ACT }}
|
||||
|
||||
|
||||
########################################################################################################################
|
||||
########################################################################################################################
|
||||
# Libs
|
||||
|
||||
lib-test:
|
||||
name: "Library test (${{ matrix.lib }})"
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
lib: [ "js", "vue" ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with: { node-version: "${{ env.NODE_VERSION }}" }
|
||||
- uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-node-${{ matrix.lib }}
|
||||
- name: "Setup YARN"
|
||||
run: npm install -g yarn @vue/cli-service
|
||||
- name: "Install dependencies"
|
||||
working-directory: lib/${{ matrix.lib }}
|
||||
run: yarn install
|
||||
- name: "Run all tests"
|
||||
working-directory: lib/${{ matrix.lib }}
|
||||
run: yarn test:unit
|
||||
|
||||
lib-build-and-publish:
|
||||
name: "Publish libraries (${{ matrix.lib }})"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [ lib-test ]
|
||||
strategy:
|
||||
matrix:
|
||||
lib: [ "js", "vue" ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
node-version: "${{ env.NODE_VERSION }}"
|
||||
- uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-node-${{ matrix.lib }}
|
||||
- name: "Setup YARN"
|
||||
run: npm install -g yarn @vue/cli-service
|
||||
- name: "Install dependencies"
|
||||
working-directory: lib/${{ matrix.lib }}
|
||||
run: yarn install
|
||||
- name: "Build Package"
|
||||
working-directory: lib/${{ matrix.lib }}
|
||||
run: yarn build
|
||||
- name: "Dry Run Publish"
|
||||
working-directory: lib/${{ matrix.lib }}
|
||||
run: npm publish --dry-run
|
||||
|
||||
# invalid secret?
|
||||
# - name: "Publish"
|
||||
# working-directory: lib/${{ matrix.lib }}
|
||||
# run: npm publish
|
||||
# env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" }
|
||||
# if: ${{ !env.ACT }}
|
||||
|
||||
|
||||
########################################################################################################################
|
||||
########################################################################################################################
|
||||
# Web clients
|
||||
|
||||
|
||||
client-web-test:
|
||||
name: "Web client test (${{ matrix.app }})"
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix: { app: [ admin, compose, discovery, privacy, reporter, one, workflow ] }
|
||||
needs: [ "lib-build-and-publish" ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with: { node-version: "${{ env.NODE_VERSION }}" }
|
||||
- name: "Setup YARN"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: npm install -g yarn @vue/cli-service
|
||||
- name: "Cache"
|
||||
uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-node-
|
||||
- name: "Dependencies"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: make dep
|
||||
- name: "Tests"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: make test
|
||||
|
||||
|
||||
client-web-release:
|
||||
name: "Web client (${{ matrix.app }})"
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix: { app: [ admin, compose, discovery, privacy, reporter, one, workflow ] }
|
||||
needs: [ client-web-test ]
|
||||
env:
|
||||
RELEASE_SFTP_KEY: ${{ secrets.RELEASE_SFTP_KEY }}
|
||||
RELEASE_SFTP_URI: ${{ secrets.RELEASE_SFTP_URI }}
|
||||
steps:
|
||||
- run: echo "BUILD_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with: { node-version: "${{ env.NODE_VERSION }}" }
|
||||
- name: "Setup YARN"
|
||||
run: npm install -g yarn @vue/cli-service
|
||||
- name: "Cache"
|
||||
uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-node-
|
||||
- name: "Dependencies"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: make dep
|
||||
- name: "Build"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: make build
|
||||
- name: "Copy essentials"
|
||||
run: cp *.md DCO LICENSE client/web/${{ matrix.app }}
|
||||
- name: "Release"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: make release
|
||||
- name: "Upload"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: make upload
|
||||
if: ${{ !env.ACT }}
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
client-web-aio-release:
|
||||
name: "Web clients (all-in-one release)"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [ client-web-release ]
|
||||
env:
|
||||
RELEASE_SFTP_KEY: ${{ secrets.RELEASE_SFTP_KEY }}
|
||||
RELEASE_SFTP_URI: ${{ secrets.RELEASE_SFTP_URI }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo "BUILD_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
- name: "Prepare dist dir"
|
||||
run: |
|
||||
mkdir -p \
|
||||
client/web/dist/admin \
|
||||
client/web/dist/compose \
|
||||
client/web/dist/workflow \
|
||||
client/web/dist/reporter \
|
||||
client/web/dist/discovery \
|
||||
client/web/dist/privacy
|
||||
- name: "Download & unpack one"
|
||||
run: |
|
||||
curl --location "${{ env.RELEASE_BASE_URL }}/corteza-webapp-one-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
tar -xzmok -C client/web/dist
|
||||
- name: "Download & unpack admin"
|
||||
run: |
|
||||
curl --silent --location "${{ env.RELEASE_BASE_URL }}/corteza-webapp-admin-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
tar -xzmok -C client/web/dist/admin
|
||||
- name: "Download & unpack compose"
|
||||
run: |
|
||||
curl --silent --location "${{ env.RELEASE_BASE_URL }}/corteza-webapp-compose-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
tar -xzmok -C client/web/dist/compose
|
||||
- name: "Download & unpack workflow"
|
||||
run: |
|
||||
curl --silent --location "${{ env.RELEASE_BASE_URL }}/corteza-webapp-workflow-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
tar -xzmok -C client/web/dist/workflow
|
||||
- name: "Download & unpack reporter"
|
||||
run: |
|
||||
curl --silent --location "${{ env.RELEASE_BASE_URL }}/corteza-webapp-reporter-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
tar -xzmok -C client/web/dist/reporter
|
||||
- name: "Download & unpack discovery"
|
||||
run: |
|
||||
curl --silent --location "${{ env.RELEASE_BASE_URL }}/corteza-webapp-discovery-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
tar -xzmok -C client/web/dist/discovery
|
||||
- name: "Download & unpack privacy"
|
||||
run: |
|
||||
curl --silent --location "${{ env.RELEASE_BASE_URL }}/corteza-webapp-privacy-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
tar -xzmok -C client/web/dist/privacy
|
||||
- name: "Copy essentials"
|
||||
run: cp *.md DCO LICENSE client/web/dist/
|
||||
- name: "Pack all web clients"
|
||||
working-directory: client/web
|
||||
run: tar -C dist -czf corteza-webapp-${{ env.BUILD_VERSION }}.tar.gz $(dir dist)
|
||||
- name: "Upload"
|
||||
working-directory: client/web
|
||||
run: |
|
||||
echo ${{ env.RELEASE_SFTP_KEY }} | base64 -d > .upload-rsa && chmod 0400 .upload-rsa
|
||||
echo "put corteza-webapp-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
sftp -q -o "StrictHostKeyChecking no" -i .upload-rsa ${{ env.RELEASE_SFTP_URI }}
|
||||
rm -f .upload-rsa
|
||||
if: ${{ !env.ACT }}
|
||||
|
||||
|
||||
########################################################################################################################
|
||||
########################################################################################################################
|
||||
# This is where it all comes together
|
||||
|
||||
|
||||
release:
|
||||
name: "Corteza (${{ matrix.os }})"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [ server-release, client-web-aio-release ]
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ "linux" ]
|
||||
env:
|
||||
ENDPOINT: https://releases.cortezaproject.org/files
|
||||
RELEASE_SFTP_KEY: ${{ secrets.RELEASE_SFTP_KEY }}
|
||||
RELEASE_SFTP_URI: ${{ secrets.RELEASE_SFTP_URI }}
|
||||
steps:
|
||||
- run: echo "BUILD_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Download"
|
||||
run: |
|
||||
curl --silent --location "${{ env.RELEASE_BASE_URL }}/corteza-server-${{ env.BUILD_VERSION }}-${{ matrix.os }}-amd64.tar.gz" | \
|
||||
tar -xzmok -C .
|
||||
mv corteza-server dist
|
||||
rm -rf dist/webapp
|
||||
mkdir dist/webapp
|
||||
curl --silent --location "${{ env.RELEASE_BASE_URL }}/corteza-webapp-${{ env.BUILD_VERSION }}.tar.gz" | \
|
||||
tar -xzmok -C dist/webapp
|
||||
- name: "Pack"
|
||||
run: |
|
||||
tar -C dist -czf corteza-${{ env.BUILD_VERSION }}-linux-amd64.tar.gz $(dir dist)
|
||||
- name: "Upload"
|
||||
run: |
|
||||
echo ${{ env.RELEASE_SFTP_KEY }} | base64 -d > .upload-rsa
|
||||
chmod 0400 .upload-rsa
|
||||
echo "put corteza-${{ env.BUILD_VERSION }}-linux-amd64.tar.gz" | \
|
||||
sftp -q -o "StrictHostKeyChecking no" -i .upload-rsa ${{ env.RELEASE_SFTP_URI }}
|
||||
rm -f .upload-rsa
|
||||
if: ${{ !env.ACT }}
|
||||
|
||||
########################################################################################################################
|
||||
########################################################################################################################
|
||||
# Discovery
|
||||
|
||||
extra-server-discovery-test:
|
||||
name: "Discovery Server test"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: "Placeholder"
|
||||
run: echo "@todo add tests"
|
||||
|
||||
extra-server-discovery-release:
|
||||
name: "Discovery Server release ${{ matrix.os }}"
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- { runsOn: "ubuntu-22.04", os: "linux" }
|
||||
- { runsOn: "macos-latest", os: "darwin" }
|
||||
runs-on: ${{ matrix.runsOn }}
|
||||
needs: [ extra-server-discovery-test ]
|
||||
env:
|
||||
BUILD_OS: ${{ matrix.os }}
|
||||
BUILD_VERSION: ${{ format(github.ref, 'refs/tags/', '') }}
|
||||
RELEASE_SFTP_KEY: ${{ secrets.RELEASE_SFTP_KEY }}
|
||||
RELEASE_SFTP_URI: ${{ secrets.RELEASE_SFTP_URI }}
|
||||
steps:
|
||||
- run: echo "BUILD_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with: { go-version: "${{ env.GO_VERSION }}" }
|
||||
- name: "Copy essentials"
|
||||
run: cp *.md DCO LICENSE extra/server-discovery/
|
||||
- name: "Build & release"
|
||||
working-directory: extra/server-discovery/
|
||||
run: make release-clean release
|
||||
- name: "Upload"
|
||||
working-directory: extra/server-discovery/
|
||||
run: make upload
|
||||
if: ${{ !env.ACT }}
|
||||
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
|
||||
docker-images:
|
||||
name: "Docker image ${{ matrix.image }}"
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- server-release
|
||||
- client-web-aio-release
|
||||
- extra-server-discovery-release
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- { context: "extra/server-discovery", image: "cortezaproject/corteza-server-discovery" }
|
||||
- { context: "client/web", image: "cortezaproject/corteza-webapp" }
|
||||
- { context: "server", image: "cortezaproject/corteza-server" }
|
||||
- { context: ".", image: "cortezaproject/corteza" }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Parse version tag"
|
||||
id: version
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
|
||||
echo "DOCKER_IMAGE_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
if [[ "$(echo ${TAG} | grep '-')" == "" ]]; then
|
||||
# when releasing patched version (YYYY.MM.PATCH) we
|
||||
# tag alias (YYYY.MM) as well
|
||||
echo "DOCKER_IMAGE_ALIAS=$(echo ${TAG} | cut -d '.' -f -2)" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: "Build"
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg "VERSION=${{ env.DOCKER_IMAGE_TAG }}" \
|
||||
--build-arg "SASS_VERSION=${{ env.SASS_VERSION }}" \
|
||||
--tag ${{ matrix.image }}:${{ env.DOCKER_IMAGE_TAG }} \
|
||||
${{ matrix.context }}
|
||||
|
||||
- name: "Login"
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ !env.ACT && env.DOCKER_IMAGE_TAG }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: "Push ${{ env.DOCKER_IMAGE_TAG }}"
|
||||
if: ${{ !env.ACT }}
|
||||
run: |
|
||||
docker push \
|
||||
${{ matrix.image }}:${{ env.DOCKER_IMAGE_TAG }}
|
||||
|
||||
- name: "Push ${{ env.DOCKER_IMAGE_ALIAS }}"
|
||||
if: ${{ !env.ACT && env.DOCKER_IMAGE_ALIAS }}
|
||||
run: |
|
||||
docker tag \
|
||||
${{ matrix.image }}:${{ env.DOCKER_IMAGE_TAG }} \
|
||||
${{ matrix.image }}:${{ env.DOCKER_IMAGE_ALIAS }}
|
||||
|
||||
docker push \
|
||||
${{ matrix.image }}:${{ env.DOCKER_IMAGE_ALIAS }}
|
||||
|
||||
notify:
|
||||
name: "Send matrix release notification"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [ docker-images, release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Parse version tag"
|
||||
id: version
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo "DOCKER_IMAGE_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: "Send message via Matrix on successful release"
|
||||
if: ${{ !env.ACT }}
|
||||
id: matrix-chat-release-successful
|
||||
uses: fadenb/matrix-chat-message@v0.0.6
|
||||
with:
|
||||
homeserver: ${{ secrets.MATRIX_HOME_SERVER }}
|
||||
token: ${{ secrets.MATRIX_ACCESS_TOKEN }}
|
||||
channel: ${{ secrets.MATRIX_ROOM_ID }}
|
||||
message: |
|
||||
# Corteza **${{ env.DOCKER_IMAGE_TAG }}** has been successfully released 🎉🎉
|
||||
|
||||
- name: "Send message via Matrix on failed release"
|
||||
if: ${{ !env.ACT && failure() }}
|
||||
id: matrix-chat-release-failed
|
||||
uses: fadenb/matrix-chat-message@v0.0.6
|
||||
with:
|
||||
homeserver: ${{ secrets.MATRIX_HOME_SERVER }}
|
||||
token: ${{ secrets.MATRIX_ACCESS_TOKEN }}
|
||||
channel: ${{ secrets.MATRIX_ROOM_ID }}
|
||||
message: |
|
||||
# Corteza **${{ env.DOCKER_IMAGE_TAG }}** release has failed
|
||||
|
||||
publish-release-sentry:
|
||||
name: "Publish release to sentry"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [ notify ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v1
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
ignore_empty: true
|
||||
ignore_missing: true
|
||||
version: ${{ github.ref_name }}
|
||||
19
.github/workflows/sonar.yml
vendored
Normal file
19
.github/workflows/sonar.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '20[0-9][0-9].[0-9].x'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v4
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
27
.github/workflows/stale-issues.yml
vendored
Normal file
27
.github/workflows/stale-issues.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Stale issue message'
|
||||
stale-pr-message: 'Stale pull request message'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
days-before-close: -1
|
||||
224
.github/workflows/test-e2e.yml
vendored
Normal file
224
.github/workflows/test-e2e.yml
vendored
Normal file
@@ -0,0 +1,224 @@
|
||||
name: 'E2e test on main branch push'
|
||||
|
||||
# These jobs can be tested with nektos/act tool
|
||||
# https://github.com/nektos/act
|
||||
#
|
||||
# Look for "!env.ACT" in the DRY_RUN expression below
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '20[0-9][0-9].[0-9].x'
|
||||
- '20[0-9][0-9].[0-9].[0-9]'
|
||||
|
||||
env:
|
||||
BUILD_OS: linux
|
||||
BUILD_ARCH: amd64
|
||||
|
||||
GO_VERSION: 1.24.1
|
||||
GOFLAGS: -mod=readonly
|
||||
|
||||
NODE_VERSION: 22
|
||||
CYPRESS_VERSION: 13.5.0
|
||||
CYPRESS_HOST: http://localhost:8888
|
||||
CYPRESS_BASE_URL: http://localhost:8888
|
||||
CYPRESS_ADMIN_URL: http://localhost:8080
|
||||
CYPRESS_COMPOSE_URL: http://localhost:8081
|
||||
CYPRESS_WORKFLOW_URL: http://localhost:8082
|
||||
CYPRESS_REPORTER_URL: http://localhost:8083
|
||||
CYPRESS_ONE_URL: http://localhost:8086
|
||||
CYPRESS_PRIVACY_URL: http://localhost:8087
|
||||
CYPRESS_USER_EMAIL: cypress@test.com
|
||||
CYPRESS_USER_PASSWORD: cypress123
|
||||
CYPRESS_USER_PASSWORD_NEW: newcypress123
|
||||
CYPRESS_USER_EMAIL_NEW: permissions@email.com
|
||||
CYPRESS_USER_DPO: dpo@email.com
|
||||
CYPRESS_USER_DPO_PASSWORD: dpo@email.com
|
||||
CYPRESS_PROVISION_DB_DSN: postgres://corteza:root@localhost:5432/corteza_cy_test?sslmode=disable
|
||||
|
||||
jobs:
|
||||
server-client-setup:
|
||||
name: server and client test
|
||||
strategy:
|
||||
matrix:
|
||||
client:
|
||||
- { port: 8080, name: admin }
|
||||
|
||||
# todo - these tests must first be provisioned in order to work,
|
||||
# only admin for now
|
||||
# - { port: 8080, name: server }
|
||||
# - { port: 8081, name: compose }
|
||||
# - { port: 8082, name: workflow }
|
||||
# - { port: 8083, name: reporter }
|
||||
# - { port: 8086, name: one }
|
||||
# - { port: 8087, name: privacy }
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Setup environment
|
||||
env:
|
||||
RELEASE_CRUST_SFTP_KEY: ${{ secrets.RELEASE_CRUST_SFTP_KEY }}
|
||||
RELEASE_CRUST_SFTP_URI: ${{ secrets.RELEASE_CRUST_SFTP_URI }}
|
||||
CLIENT_NAME: ${{ matrix.client.name }}
|
||||
run: |
|
||||
echo "TMP_KEY=$(mktemp)" >> $GITHUB_ENV
|
||||
|
||||
- name: Create crust key
|
||||
env:
|
||||
RELEASE_CRUST_SFTP_KEY: ${{ secrets.RELEASE_CRUST_SFTP_KEY }}
|
||||
RELEASE_CRUST_KEY_FILE: ${{ env.TMP_KEY }}
|
||||
run: |
|
||||
echo ${RELEASE_CRUST_SFTP_KEY} | base64 -d > ${RELEASE_CRUST_KEY_FILE} && \
|
||||
echo ${RELEASE_CRUST_SFTP_KEY} > /tmp/foo
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with: { node-version: '${{ env.NODE_VERSION }}' }
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
with: { go-version: '${{ env.GO_VERSION }}' }
|
||||
|
||||
- name: Setup YARN
|
||||
run: npm install -g yarn @vue/cli-service
|
||||
|
||||
- name: Checkout corteza repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: corteza
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Clone CY repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: cortezaproject/corteza-e2e-cypress
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: 2024.9.x
|
||||
path: corteza-e2e-cypress
|
||||
|
||||
- name: Cache lib dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
corteza/lib/vue/node_modules
|
||||
corteza/lib/js/node_modules
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('corteza/lib/vue/yarn.lock', 'corteza/lib/js/yarn.lock') }}
|
||||
|
||||
- name: Copy server language files
|
||||
working-directory: corteza/server/pkg/locale
|
||||
run: make src/en
|
||||
|
||||
- name: Copy assets
|
||||
working-directory: corteza
|
||||
env:
|
||||
CLIENT_NAME: ${{ matrix.client.name }}
|
||||
run: |
|
||||
mv ./.github/workflows/assets/docker-compose.yaml ./
|
||||
cp .github/workflows/assets/client/vue.config-builder.js client/web/${CLIENT_NAME}/public/vue.config-builder.js
|
||||
cp .github/workflows/assets/client/config.js client/web/${CLIENT_NAME}/public/config.js
|
||||
|
||||
docker compose config > /tmp/docker-compose.corteza.yaml
|
||||
cd ../corteza-e2e-cypress && docker compose config > /tmp/docker-compose.corteza-e2e.yaml
|
||||
|
||||
# - name: Cache Docker images
|
||||
# uses: ScribeMD/docker-cache@0.3.7
|
||||
# with:
|
||||
# key: docker-${{ runner.os }}-${{ hashFiles('/tmp/docker-compose.corteza.yaml', '/tmp/docker-compose.corteza-e2e.yaml') }}
|
||||
# # Only cache Corteza-specific images, not common base images already on runners
|
||||
# images: |
|
||||
# cypress/included:13.5.0
|
||||
|
||||
- name: Run server and db
|
||||
working-directory: corteza
|
||||
run: |
|
||||
docker compose run -d --name backend backend
|
||||
docker compose run -d --name postgres postgres
|
||||
|
||||
- name: Build js
|
||||
working-directory: corteza/lib/js
|
||||
run: |
|
||||
yarn install
|
||||
yarn build
|
||||
|
||||
- name: Build vue
|
||||
working-directory: corteza/lib/vue
|
||||
run: |
|
||||
yarn install
|
||||
yarn build
|
||||
|
||||
- name: Run ${{ matrix.client.name }}
|
||||
working-directory: corteza
|
||||
# re-link the packages for each app
|
||||
env:
|
||||
CLIENT_NAME: ${{ matrix.client.name }}
|
||||
CLIENT_PORT: ${{ matrix.client.port }}
|
||||
run: |
|
||||
docker compose \
|
||||
run -d \
|
||||
--name "${CLIENT_NAME}" \
|
||||
--use-aliases \
|
||||
client \
|
||||
sh -c "ls && cd /corteza/lib/js && yarn link && \
|
||||
cd /corteza/lib/vue && yarn link && \
|
||||
cd /corteza/client/web/${CLIENT_NAME} && \
|
||||
yarn link @cortezaproject/corteza-vue && \
|
||||
yarn link @cortezaproject/corteza-js && \
|
||||
yarn install && \
|
||||
yarn serve --port ${CLIENT_PORT}"
|
||||
|
||||
# github actions do not support docker-compose --wait
|
||||
- name: Wait for postgres healthcheck
|
||||
run: timeout 180s sh -c 'until docker ps | grep postgres | grep -q healthy; do echo "Waiting for container to be healthy..."; sleep 1; done'
|
||||
|
||||
- name: Wait for ${{ matrix.client.name }} healthcheck
|
||||
env:
|
||||
CLIENT_NAME: ${{ matrix.client.name }}
|
||||
run: |
|
||||
timeout 180s sh -c 'until docker ps | grep ${CLIENT_NAME} | grep -q healthy; do echo "Waiting for container to be healthy..."; sleep 1; done'
|
||||
|
||||
- name: Wait for backend healthcheck
|
||||
run: timeout 180s sh -c 'until docker ps | grep backend | grep -q healthy; do echo "Waiting for container to be healthy..."; sleep 1; done'
|
||||
|
||||
- name: Run CY tests for ${{ matrix.client.name }}
|
||||
env:
|
||||
CLIENT_NAME: ${{ matrix.client.name }}
|
||||
CLIENT_PORT: ${{ matrix.client.port }}
|
||||
working-directory: corteza-e2e-cypress
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
docker compose run \
|
||||
--entrypoint="bash -c \" \
|
||||
cypress run --spec cypress/e2e/basic-functionalities/${CLIENT_NAME}/index.cy.js --browser chrome --config video=false,screenshotOnRunFailure=false \
|
||||
\"" \
|
||||
cypress
|
||||
|
||||
- name: Publish results
|
||||
# always() runs the step even when canceled manually
|
||||
if: success() || failure()
|
||||
working-directory: corteza-e2e-cypress
|
||||
env:
|
||||
RELEASE_CRUST_SFTP_URI: ${{ secrets.RELEASE_CRUST_SFTP_URI }}
|
||||
RELEASE_CRUST_KEY_FILE: ${{ env.TMP_KEY }}
|
||||
run: |
|
||||
npx mochawesome-merge "cypress/reports/*.json" > cypress/report.json && \
|
||||
npx marge -o cypress/reports-build -f index.html -i cypress/report.json && \
|
||||
echo "put cypress/reports-build/index.html" | sftp -q -o "StrictHostKeyChecking no" -i ${RELEASE_CRUST_KEY_FILE} ${RELEASE_CRUST_SFTP_URI}
|
||||
|
||||
notify-failure:
|
||||
needs: [server-client-setup]
|
||||
runs-on: ubuntu-22.04
|
||||
if: failure()
|
||||
steps:
|
||||
- name: Send message on failed testing results
|
||||
uses: fadenb/matrix-chat-message@v0.0.6
|
||||
env:
|
||||
E2E_REPORTS_USER: ${{ secrets.E2E_REPORTS_USER }}
|
||||
E2E_REPORTS_PASS: ${{ secrets.E2E_REPORTS_PASS }}
|
||||
SCREENSHOTS_GEN_FOLDER: ${{ env.SCREENSHOTS_GEN_FOLDER }}
|
||||
with:
|
||||
homeserver: ${{ secrets.MATRIX_HOME_SERVER }}
|
||||
token: ${{ secrets.MATRIX_ACCESS_TOKEN }}
|
||||
channel: ${{ secrets.MATRIX_ROOM_ID }}
|
||||
message: |
|
||||
❌ E2e tests did not pass
|
||||
|
||||
🔗 https://${{ secrets.E2E_REPORTS_USER }}:${{ secrets.E2E_REPORTS_PASS }}@releases.cortezaproject.org/e2e-reports/
|
||||
115
.github/workflows/test.yml
vendored
Normal file
115
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: "Server and lib tests"
|
||||
|
||||
# These jobs can be tested with nektos/act tool
|
||||
# https://github.com/nektos/act
|
||||
#
|
||||
# Look for "!env.ACT" in the DRY_RUN expression below
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
push:
|
||||
branches:
|
||||
- '20[0-9][0-9].[0-9].x'
|
||||
- '20[0-9][0-9].[0-9].[0-9]'
|
||||
|
||||
env:
|
||||
BUILD_OS: linux
|
||||
BUILD_ARCH: amd64
|
||||
|
||||
GO_VERSION: 1.24.1
|
||||
GOFLAGS: -mod=readonly
|
||||
WORKFLOW_STACK_TRACE_FULL: true
|
||||
|
||||
NODE_VERSION: 22
|
||||
|
||||
jobs:
|
||||
lib-test:
|
||||
name: "Library test (${{ matrix.lib }})"
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
lib: [ "js", "vue" ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with: { node-version: "${{ env.NODE_VERSION }}" }
|
||||
- uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-node-${{ matrix.lib }}
|
||||
- name: "Setup YARN"
|
||||
run: npm install -g yarn @vue/cli-service
|
||||
- name: "Install dependencies"
|
||||
working-directory: lib/${{ matrix.lib }}
|
||||
run: yarn install
|
||||
- name: "Run all tests"
|
||||
working-directory: lib/${{ matrix.lib }}
|
||||
run: yarn test:unit
|
||||
|
||||
client-web-test:
|
||||
name: "Web client test (${{ matrix.app }})"
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix: { app: [ admin, compose, discovery, privacy, reporter, one, workflow ] }
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with: { node-version: "${{ env.NODE_VERSION }}" }
|
||||
- name: "Setup YARN"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: npm install -g yarn @vue/cli-service
|
||||
- name: "Cache"
|
||||
uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-node-
|
||||
- name: "Dependencies"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: make dep
|
||||
- name: "Tests"
|
||||
working-directory: client/web/${{ matrix.app }}
|
||||
run: make test
|
||||
|
||||
server-test:
|
||||
name: "Server tests"
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
GOFLAGS: -mod=readonly
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with: { go-version: "${{ env.GO_VERSION }}" }
|
||||
- name: "Copy language files"
|
||||
working-directory: server/pkg/locale
|
||||
run: make src/en
|
||||
- name: "Unit"
|
||||
working-directory: server
|
||||
run: make test.unit
|
||||
- name: "Store"
|
||||
working-directory: server
|
||||
run: make test.store
|
||||
- name: "Integration"
|
||||
working-directory: server
|
||||
run: make test.integration
|
||||
|
||||
notify:
|
||||
if: failure()
|
||||
name: "Send matrix testing notification"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [ server-test, client-web-test, lib-test ]
|
||||
steps:
|
||||
- name: "Send message via Matrix on failed tests"
|
||||
if: ${{ !env.ACT && failure() }}
|
||||
id: matrix-chat-tests-failed
|
||||
uses: fadenb/matrix-chat-message@v0.0.6
|
||||
with:
|
||||
homeserver: ${{ secrets.MATRIX_HOME_SERVER }}
|
||||
token: ${{ secrets.MATRIX_ACCESS_TOKEN }}
|
||||
channel: ${{ secrets.MATRIX_ROOM_ID }}
|
||||
message: |
|
||||
# Corteza **${GITHUB_REF##*/}** tests have failed
|
||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
.run
|
||||
node_modules
|
||||
ignore
|
||||
69
CONTRIBUTING.md
Normal file
69
CONTRIBUTING.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for helping us make [our vision](https://cortezaproject.org/about/what-is-corteza/) a reality!
|
||||
All contributions are welcome; from bug reports, codefixes, and new features!
|
||||
|
||||
## Ground Rules
|
||||
|
||||
Corteza projects are [Apache 2.0 licensed](LICENSE) and accept contributions via GitHub pull requests.
|
||||
|
||||
Cover the [terminology](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/release-cycle/index.html#_terminology) for the development process and versioning.
|
||||
|
||||
Cover the [Git and GitHub](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/release-cycle/index.html#_github) ground rules regarding branch naming and conventions.
|
||||
|
||||
When you wish to start working on a code contribution, assign yourself to a GitHub issue.
|
||||
If there is no issue, create one beforehand.
|
||||
|
||||
A quick summary on [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/):
|
||||
|
||||
1. Separate subject from body with a blank line
|
||||
2. Limit the subject line to 50 characters
|
||||
3. Capitalize the subject line
|
||||
4. Do not end the subject line with a period
|
||||
5. Use the imperative mood in the subject line
|
||||
6. Wrap the body at 72 characters
|
||||
7. Use the body to explain what and why vs. how
|
||||
|
||||
## Core repositories
|
||||
|
||||
### Corteza Server
|
||||
|
||||
Corteza server is the back-end of the Corteza ecosystem.
|
||||
The core logic is written in GO, using [go-chi](https://pkg.go.dev/github.com/go-chi/chi@v3.3.4+incompatible?utm_source=gopls) for the routing.
|
||||
|
||||
Communication between the Corteza server and web applications is done using the REST API and web sockets.
|
||||
Communication between back-end services (Corteza server and Corredor) is done using gRPC.
|
||||
|
||||
The [Developer Guide/Corteza Server](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/corteza-server/index.html) covers the [development setup](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/corteza-server/index.html#_development_setup), the [project structure](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/corteza-server/structure.html), and the feature insight documents.
|
||||
|
||||
|
||||
### Corteza Web Applications
|
||||
|
||||
The web applications are written in Vue.js and provide the user interface to interact with the entire system.
|
||||
|
||||
Communication between the Corteza server and web applications is done using the REST API and web sockets.
|
||||
|
||||
The [Developer Guide/Corteza Web Applications](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/web-applications/index.html) covers the [development setup](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/web-applications/index.html#_development_setup), the [project structure](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/web-applications/structure.html), and the feature insight documents.
|
||||
|
||||
### Documentation
|
||||
|
||||
The documentation is written in [AsciiDoc](https://asciidoc.org/) and compiled using [Antora](https://antora.org/).
|
||||
The source code is available on the [GitHub cortezaproject/corteza-docs repository](https://github.com/cortezaproject/corteza-docs); the generated output is available on the [documentation page](http://docs.cortezaproject.org/).
|
||||
|
||||
The [Developer Guide/Documentation](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/documentation/index.html) covers the [conventions](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/documentation/index.html#_conventions), [writing guidelines](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/documentation/index.html#documentation-writing-guidelines), as well as some [examples](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/documentation/examples/index.html) to help you get started.
|
||||
|
||||
## Bug reporting
|
||||
|
||||
Please submit any bug reports on the **issues** section of the corresponding GitHub repository.
|
||||
If you are unsure where to submit the issue, or you are unsure if this is a feature; reach out to us on [our forum](https://forum.cortezaproject.org/).
|
||||
|
||||
## Feature requests
|
||||
|
||||
Feature and improvement requests should be submitted on [our forum](https://forum.cortezaproject.org/).
|
||||
Before opening a new topic, search around to see if there are any similar topics that would cover your case.
|
||||
|
||||
## DCO
|
||||
|
||||
By contributing to this project you agree to the Developer Certificate of Origin (DCO).
|
||||
This document was created by the Linux Kernel community and is a simple statement that you, as a contributor, have the legal right to make the contribution.
|
||||
See the [DCO](DCO) file for details.
|
||||
36
DCO
Normal file
36
DCO
Normal file
@@ -0,0 +1,36 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
71
Dockerfile
Normal file
71
Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
||||
# build-stage
|
||||
FROM alpine:3 as build-stage
|
||||
|
||||
# use docker build --build-arg VERSION=2021.9.0 .
|
||||
ARG VERSION=2022.9.0
|
||||
ARG SASS_VERSION=1.69.5
|
||||
ARG SERVER_VERSION=${VERSION}
|
||||
ARG WEBAPP_VERSION=${VERSION}
|
||||
ARG CORTEZA_SERVER_PATH=https://releases.cortezaproject.org/files/corteza-server-${SERVER_VERSION}-linux-amd64.tar.gz
|
||||
ARG CORTEZA_WEBAPP_PATH=https://releases.cortezaproject.org/files/corteza-webapp-${WEBAPP_VERSION}.tar.gz
|
||||
ARG SASS_URL=https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-linux-x64.tar.gz
|
||||
|
||||
RUN mkdir /tmp/server
|
||||
RUN mkdir /tmp/webapp
|
||||
|
||||
ADD $CORTEZA_SERVER_PATH /tmp/server
|
||||
ADD $CORTEZA_WEBAPP_PATH /tmp/webapp
|
||||
|
||||
RUN apk update && apk add --no-cache file
|
||||
RUN apk add curl
|
||||
|
||||
RUN file "/tmp/server/$(basename $CORTEZA_SERVER_PATH)" | grep -q 'gzip' && \
|
||||
tar zxvf "/tmp/server/$(basename $CORTEZA_SERVER_PATH)" -C / || \
|
||||
cp -a "/tmp/server" /
|
||||
|
||||
RUN mv /corteza-server /corteza
|
||||
|
||||
WORKDIR /corteza
|
||||
|
||||
RUN rm -rf /corteza/webapp
|
||||
|
||||
RUN file "/tmp/webapp/$(basename $CORTEZA_WEBAPP_PATH)" | grep -q 'gzip' && \
|
||||
mkdir /corteza/webapp && tar zxvf "/tmp/webapp/$(basename $CORTEZA_WEBAPP_PATH)" -C /corteza/webapp || \
|
||||
cp -a "/tmp/webapp" /corteza/webapp
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
RUN curl -sOL $SASS_URL
|
||||
RUN tar -xzf dart-sass-${SASS_VERSION}-linux-x64.tar.gz
|
||||
|
||||
# deploy-stage
|
||||
FROM ubuntu:22.04
|
||||
|
||||
RUN apt-get -y update \
|
||||
&& apt-get -y install \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV STORAGE_PATH "/data"
|
||||
ENV CORREDOR_ADDR "corredor:80"
|
||||
ENV HTTP_ADDR "0.0.0.0:80"
|
||||
ENV HTTP_WEBAPP_ENABLED "true"
|
||||
ENV HTTP_WEBAPP_BASE_DIR "/corteza/webapp"
|
||||
ENV PATH "/opt/dart-sass:/corteza/bin:${PATH}"
|
||||
|
||||
WORKDIR /corteza
|
||||
|
||||
VOLUME /data
|
||||
|
||||
COPY --from=build-stage /corteza ./
|
||||
COPY --from=build-stage /tmp/dart-sass /opt/dart-sass
|
||||
|
||||
HEALTHCHECK --interval=30s --start-period=1m --timeout=30s --retries=3 \
|
||||
CMD curl --silent --fail --fail-early http://127.0.0.1:80/healthcheck || exit 1
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["./bin/corteza-server"]
|
||||
|
||||
CMD ["serve-api"]
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
36
Makefile
Normal file
36
Makefile
Normal file
@@ -0,0 +1,36 @@
|
||||
.PHONY: dev test lint fresh audit
|
||||
|
||||
dev:
|
||||
@echo "---Processing libs---"
|
||||
@(cd $(CURDIR)/lib && make dev) || (echo "Failed to build libs"; exit 1)
|
||||
@echo "---Processing clients---"
|
||||
@(cd $(CURDIR)/client && make dev) || (echo "Failed to yarn clients"; exit 1)
|
||||
|
||||
test:
|
||||
@echo "---Testing libs---"
|
||||
@(cd $(CURDIR)/lib && make test) || (echo "Failed to test libs"; exit 1)
|
||||
@echo "---Testing clients---"
|
||||
@(cd $(CURDIR)/client && make test) || (echo "Failed to test clients"; exit 1)
|
||||
@echo "---Testing server---"
|
||||
@(cd $(CURDIR)/server && make test) || (echo "Failed to test server"; exit 1)
|
||||
|
||||
lint:
|
||||
@echo "---Linting libs---"
|
||||
@(cd $(CURDIR)/lib && make lint) || (echo "Failed to lint libs"; exit 1)
|
||||
@echo "---Linting clients---"
|
||||
@(cd $(CURDIR)/client && make lint) || (echo "Failed to lint clients"; exit 1)
|
||||
|
||||
fresh:
|
||||
@echo "---Fresh---"
|
||||
@(cd $(CURDIR)/lib && make fresh) || (echo "Failed to fresh libs"; exit 1)
|
||||
@echo "---Fresh clients---"
|
||||
@(cd $(CURDIR)/client && make fresh) || (echo "Failed to fresh clients"; exit 1)
|
||||
|
||||
|
||||
audit:
|
||||
@echo "---Audit---"
|
||||
@(cd $(CURDIR)/lib && make audit) || true
|
||||
@echo "---Audit clients---"
|
||||
@(cd $(CURDIR)/client && make audit) || true
|
||||
|
||||
.DEFAULT_GOAL := dev
|
||||
85
README.md
Normal file
85
README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
<h1 align="center">
|
||||
<img width="300px" src=".github/assets/corteza_logo.svg" />
|
||||
<br />
|
||||
<br />
|
||||
<abbr>Freedom. Flexibility. Performance. Speed.</abbr>
|
||||
<br />
|
||||
<br />
|
||||
<div align="center">
|
||||
|
||||
[](https://img.shields.io/github/license/cortezaproject/corteza?style=for-the-badge)
|
||||
[](https://img.shields.io/docker/pulls/cortezaproject/corteza?style=for-the-badge)
|
||||
[](https://img.shields.io/github/v/release/cortezaproject/corteza?label=stable%20version&style=for-the-badge)
|
||||
|
||||
[](https://goreportcard.com/report/github.com/cortezaproject/corteza?style=for-the-badge)
|
||||
[](https://img.shields.io/codecov/c/github/cortezaproject/corteza-server?style=for-the-badge)
|
||||
|
||||
[](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fforum.cortezaproject.org%2F&style=for-the-badge)
|
||||
|
||||
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
The Corteza low-code platform lets you build and iterate CRM, business process and other structured data apps fast, create intelligent business process workflows and connect with almost any data source.
|
||||
All with the flexibility, freedom and control you get from a 100% open-source platform written on modern technologies maintained by [Planet Crust](https://www.planetcrust.com/), its founder.
|
||||
|
||||
<div align="center">
|
||||
<img src=".github/assets/hero-animation.webp">
|
||||
</div>
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Standards oriented** by using well established data formats, technologies, and design decisions.
|
||||
* **Flexible security** provided as a flattened RBAC facility allows organizations to apply complex internal security policies to Corteza.
|
||||
* **Extensive privacy features** provide a way for organizations to configure Corteza to conform to data privacy regulations in the regions they are operating in.
|
||||
* **Accessible** conforming to the WCAG 2.1 guidelines allowing simple use for a wider audience.
|
||||
* **Integratable** into and with external services and other Corteza instances due to it's API-centric design, flexible data storage layer, and powerful automation facilities.
|
||||
* **Customizable** from translations and terminology to prebuilt low-code applications to completely new low-code applications and business processes.
|
||||
|
||||
## Getting Started
|
||||
|
||||
<br />
|
||||
|
||||
> **Note**
|
||||
> Planet Crust offers hosted [Corteza cloud instances](https://www.planetcrust.com/start-trial-lp-main) as the easiest way to get started.
|
||||
|
||||
<br />
|
||||
|
||||
Refer to the [DevOps guide](https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/index.html) for a complete guide on how to get Corteza up and running.
|
||||
Additionally, we've provided some [video instructions](https://forum.cortezaproject.org/t/videos-on-how-to-set-up-corteza/91).
|
||||
|
||||
Quick references:
|
||||
* [data backup and restore](https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/maintenance/backups.html)
|
||||
* [troubleshooting](https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/troubleshooting/index.html)
|
||||
* [setting up an email relay](https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/email-relay.html)
|
||||
* [setting up sink routes](https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/sink-route.html)
|
||||
* [offline](https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/examples/deploy-offline/index.html) and [online](https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/examples/deploy-online/index.html) configuration examples
|
||||
* [online demo instance](https://latest.cortezaproject.org/) for a hands-on feature exploration
|
||||
|
||||
## Creating With Corteza
|
||||
|
||||
Refer to the [Integrator Guide](https://docs.cortezaproject.org/corteza-docs/2024.9/integrator-guide/index.html) to learn how you can build on the core features to create virtually anything.
|
||||
|
||||
Quick references:
|
||||
* [Corteza Compose configuration](https://docs.cortezaproject.org/corteza-docs/2024.9/integrator-guide/compose-configuration/index.html)
|
||||
* automation using [workflows](https://docs.cortezaproject.org/corteza-docs/2024.9/integrator-guide/automation/workflows/index.html) and [automation scripts](https://docs.cortezaproject.org/corteza-docs/2024.9/integrator-guide/automation/automation-scripts/index.html)
|
||||
* [using the REST API](https://docs.cortezaproject.org/corteza-docs/2024.9/integrator-guide/accessing-corteza/index.html),
|
||||
|
||||
## Upgrading Corteza
|
||||
|
||||
Refer to the [changelog](https://docs.cortezaproject.org/corteza-docs/2024.9/changelog/index.html) and the [upgrade guide](https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/upgrade/index.html) to upgrade your Corteza instance.
|
||||
|
||||
## Contributing
|
||||
|
||||
Refer to the [Developer Guide/Corteza Server](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/corteza-server/index.html) document for details regarding the [development setup](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/corteza-server/index.html#_development_setup), the [project structure](https://docs.cortezaproject.org/corteza-docs/2024.9/developer-guide/corteza-server/structure.html), and the feature insight documents.
|
||||
|
||||
Refer to the [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and code of conduct.
|
||||
|
||||
## Community
|
||||
|
||||
Reach out to us on [our forum](https://forum.cortezaproject.org/).
|
||||
|
||||
## License
|
||||
|
||||
Corteza is released under the Apache-2.0 license.
|
||||
Refer to the [LICENSE](LICENSE) file for additional information.
|
||||
37
SECURITY.md
Normal file
37
SECURITY.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Security
|
||||
|
||||
At Corteza, the security of our products and services is important to us.
|
||||
All of our source code repositories are managed through GitHub organisations.
|
||||
Here is the list of [Corteza Repositories](https://github.com/orgs/cortezaproject/repositories)
|
||||
|
||||
If you believe you have found a security vulnerability in any Corteza repository, please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please send email to [info@cortezaproject.org](mailto:info@cortezaproject.org).
|
||||
|
||||
You should receive a response within 24 business hours. If for some reason you do not,
|
||||
please follow up via email to ensure we received your original message.
|
||||
|
||||
Please include the requested information listed below (as much as you can provide)
|
||||
to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue,
|
||||
* full paths of source file(s) related to the manifestation of the issue,
|
||||
* the location of the affected source code (tag/branch/commit or direct URL),
|
||||
* any special configuration required to reproduce the issue,
|
||||
* step-by-step instructions to reproduce the issue,
|
||||
* proof-of-concept or exploit code (if possible),
|
||||
* impact of the issue, including how an attacker might exploit the issue.
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
[Corteza Privacy Policy](https://cortezaproject.org/privacy-policy/)
|
||||
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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user